From 0c6e1591d6c44e7e0bc2a3db430562dd281e5e8f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 24 Apr 2026 20:30:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5=D0=BF=D0=BE?= =?UTF-8?q?=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 + .github/workflows/rebalance.yml | 40 +++++ .gitignore | 14 ++ .python-version | 1 + LICENCE | 21 +++ README.md | 168 ++++++++++++++++++ app/load_config.py | 38 ++++ app/notifier.py | 27 +++ app/rebalance.py | 223 ++++++++++++++++++++++++ config/target.yaml | 14 ++ main.py | 62 +++++++ pyproject.toml | 12 ++ requirements.txt | 4 + tools/get_instrument.py | 158 +++++++++++++++++ uv.lock | 296 ++++++++++++++++++++++++++++++++ 15 files changed, 1082 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/rebalance.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 LICENCE create mode 100644 README.md create mode 100644 app/load_config.py create mode 100644 app/notifier.py create mode 100644 app/rebalance.py create mode 100644 config/target.yaml create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 tools/get_instrument.py create mode 100644 uv.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dc53a9e --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +TINVEST_TOKEN=t.*** +TINVEST_ACCOUNT_ID=your_account_id +TELEGRAM_BOT_TOKEN=your_telegram_bot_token +TELEGRAM_USER_ID=your_telegram_user_id \ No newline at end of file diff --git a/.github/workflows/rebalance.yml b/.github/workflows/rebalance.yml new file mode 100644 index 0000000..3ddc405 --- /dev/null +++ b/.github/workflows/rebalance.yml @@ -0,0 +1,40 @@ +name: Weekly Portfolio Rebalance + +on: + # 1. Запуск по расписанию (каждый понедельник в 10:30 МСК) + schedule: + - cron: '30 7 * * 1' # Время в UTC (7:30 UTC = 10:30 МСК) + + # 2. Возможность запустить вручную кнопкой в интерфейсе GitHub + workflow_dispatch: + inputs: + dry_run: + description: 'Режим симуляции (без реальных сделок)' + required: true + type: boolean + default: true + +jobs: + rebalance: + runs-on: ubuntu-latest + environment: tinvest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Set up Python + run: uv python install 3.13 + + - name: Run Rebalance Bot + env: + # Передаем секреты в переменные окружения скрипта + TINVEST_TOKEN: ${{ secrets.TINVEST_TOKEN }} + TINVEST_ACCOUNT_ID: ${{ secrets.TINVEST_ACCOUNT_ID }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_USER_ID: ${{ secrets.TELEGRAM_USER_ID }} + DRY_RUN_OVERRIDE: ${{ github.event.inputs.dry_run }} + run: uv run main.py \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b347aca --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.env + +# IDE / AI assistant config +.claude/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..42f86e4 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dmitry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca1bd01 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# etf-gyro + +Бот для автоматического ребалансирования инвестиционного портфеля через API T-Invest (Т-Инвестиции). + +Бот рассчитан на балансировку ETF-портфеля (т.е. с использованием паевых фондов для гибкости долей), но при большом портфеле можно использовать и для иных фин. инструментов. + +## Возможности + +- Расчёт текущих долей активов и сравнение с целевыми весами +- Автоматическое планирование покупок и продаж при отклонении от цели +- Коридор допустимого отклонения — мелкие расхождения игнорируются +- Автоматическая продажа активов, не входящих в конфиг +- Распределение избытка рублёвого кэша пропорционально целевым весам +- Режим dry-run — расчёт плана без реальных сделок +- Уведомления в Telegram с отчётом по каждому запуску + +## Требования + +- Python 3.13+ +- Токен T-Invest API +- Telegram-бот + +## Установка + +```bash +git clone https://github.com/ada-dmitry/etf-gyro.git +cd etf-gyro +pip install -r requirements.txt +``` + +Или через `uv`: + +```bash +uv sync +``` + +## Конфигурация + +Скопируй `.env.example` в `.env` и заполни: + +```env +TINVEST_TOKEN=t.your_token_here +TINVEST_ACCOUNT_ID=your_account_id +TELEGRAM_BOT_TOKEN=your_telegram_bot_token +TELEGRAM_USER_ID=your_telegram_user_id +``` + +- **TINVEST_TOKEN** — токен T-Invest API: [Настройки → Токены](https://www.tbank.ru/invest/settings/) +- **TINVEST_ACCOUNT_ID** — ID счёта; узнать через `python tools/get_instrument.py accounts` +- **TELEGRAM_BOT_TOKEN** — создать бота через [@BotFather](https://t.me/botfather) +- **TELEGRAM_USER_ID** — узнать через [@userinfobot](https://t.me/userinfobot) + +## Настройка портфеля + +Отредактируй `config/target.yaml`: + +```yaml +portfolio: + TMOS@: + name: "Крупнейшие компании РФ" + figi: "TCS60A101X76" + uid: "f509af83-6e71-462f-901f-bcb073f6773b" + weight: 0.98 + + RUB: + name: "Рублевый кэш" + figi: "RUB000UTSTOM" + uid: "a92e2e25-a698-45cc-a781-167cf465257c" + weight: 0.02 + +corridor: 0.02 +``` + +- `uid` — уникальный идентификатор инструмента в T-Invest; найти через `tools/get_instrument.py yaml ` +- `weight` — целевая доля (0.0–1.0); сумма всех весов должна быть ≈ 1.0 +- `corridor` — порог отклонения, ниже которого сделка не создаётся (0.02 = 2%) +- `RUB` — специальный тикер для кэша; избыток сверх `weight` автоматически распределяется по остальным активам + +## Запуск + +Утилиту можно запускать локально, если хочется контроля над операциями (сначала прогнать на dry-run, а потом уже на реальных сделках). + +```bash +# Dry-run — только расчёт плана, без реальных сделок +DRY_RUN_OVERRIDE=true python main.py + +# С реальными сделками (по умолчанию) +python main.py +``` + +## Автоматизация через cron + +Для запуска на сервере (VPS, HomeLab и др.) задание в crontab: + +```bash +crontab -e +``` + +```cron +# Ребалансировка каждый понедельник в 10:30 +30 10 * * 1 cd /path/to/etf-gyro && /path/to/uv run main.py >> /var/log/gyro.log 2>&1 +``` + +Пути к `uv` и проекту — абсолютные, иначе cron не найдёт их в своём окружении. Узнать путь к `uv`: `which uv`. + +Переменные окружения cron не наследует из шелла, поэтому `.env` файл должен быть заполнен — скрипт читает его через `python-dotenv`. + +Проверить, что задание отработало: + +```bash +tail -f /var/log/gyro.log +``` + +## Автоматизация через GitHub Actions + +Если своего сервера нет или хочется держать на стабильной инфраструктуре, то можно использовать GitHub Actions. + +Воркфлоу `.github/workflows/rebalance.yml` запускает ребалансировку каждый понедельник в 10:30 МСК. + +Добавь в настройках репозитория → **Settings → Environments** окружение `tinvest` и пропиши секреты: + +``` +TINVEST_TOKEN +TINVEST_ACCOUNT_ID +TELEGRAM_BOT_TOKEN +TELEGRAM_USER_ID +``` + +Запуск вручную доступен через **Actions → Weekly Portfolio Rebalance → Run workflow**, с возможностью включить режим симуляции. + +## Утилита поиска инструментов + +`tools/get_instrument.py` — CLI для работы с T-Invest API без запуска основного бота: + +```bash +# Список счетов и их ID +python tools/get_instrument.py accounts + +# Поиск инструмента по тикеру, ISIN или названию +python tools/get_instrument.py search TMOS@ +python tools/get_instrument.py search "Тинькофф" -n 5 + +# Готовый сниппет для вставки в config/target.yaml +python tools/get_instrument.py yaml TMOS@ + +# Текущие позиции портфеля с долями и стоимостью +python tools/get_instrument.py portfolio +python tools/get_instrument.py portfolio --account +``` + +## Структура проекта + +``` +etf-gyro/ +├── main.py # Точка входа +├── config/ +│ └── target.yaml # Целевые веса активов и коридор +├── app/ +│ ├── load_config.py # Загрузка конфига из YAML +│ ├── notifier.py # Telegram-уведомления +│ └── rebalance.py # Логика ребалансирования +└── tools/ + └── get_instrument.py # CLI: search / yaml / accounts / portfolio +``` + +## Лицензия + +MIT diff --git a/app/load_config.py b/app/load_config.py new file mode 100644 index 0000000..c5ce121 --- /dev/null +++ b/app/load_config.py @@ -0,0 +1,38 @@ +import yaml +from decimal import Decimal +from typing import Dict, Any + + +def load_config(config_path: str = "config/target.yaml") -> Dict[str, Any]: + with open(config_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + # Результирующий словарь для портфеля + portfolio = {} + corridor = Decimal(str(data.get("corridor", 0))) + + # Проходим по всем инструментам в блоке portfolio + for ticker, info in data.get("portfolio", {}).items(): + # Важно: переводим weight в Decimal сразу при чтении + portfolio[ticker] = { + "name": info.get("name"), + "figi": info.get("figi"), + "uid": info.get("uid"), + "weight": Decimal(str(info.get("weight", 0))), + } + + # Проверка суммы весов + total_weight = sum(item["weight"] for item in portfolio.values()) + if abs(total_weight - Decimal("1.0")) > Decimal("0.0001"): + print( + f"⚠️ ВНИМАНИЕ: Сумма весов в конфиге = {total_weight}. Проверьте коэффициенты!" + ) + + return portfolio, corridor # type: ignore + + +if __name__ == "__main__": + config, corridor = load_config() + for ticker, info in config.items(): # type: ignore + print(f"{ticker}: {info}") + print(f"Corridor: {corridor}") diff --git a/app/notifier.py b/app/notifier.py new file mode 100644 index 0000000..01e3610 --- /dev/null +++ b/app/notifier.py @@ -0,0 +1,27 @@ +import logging +from typing import Optional +import requests + + +class Notifier: + + def __init__(self, token: str): + self.token = token + self.api_url = f"https://api.telegram.org/bot{token}" + self.logger = logging.getLogger(__name__) + + def send_message(self, user_id: int, text: str) -> Optional[dict]: + try: + url = f"{self.api_url}/sendMessage" + payload = { + "chat_id": user_id, + "text": text, + "parse_mode": "HTML", + "disable_web_page_preview": True, + } + response = requests.post(url, json=payload, timeout=10) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + self.logger.error(f"Failed to send message to {user_id}: {e}") + return None diff --git a/app/rebalance.py b/app/rebalance.py new file mode 100644 index 0000000..feba719 --- /dev/null +++ b/app/rebalance.py @@ -0,0 +1,223 @@ +from decimal import Decimal +from t_tech.invest import OrderDirection, OrderType + +from t_tech.invest.utils import quotation_to_decimal as to_dec +import uuid + + +class RebalanceBot: + def __init__( + self, + client, + account_id: str, + target_weights: dict, + corridor: Decimal, + dry_run=True, + ): + + self.client = client + self.account_id = account_id + self.target_weights = target_weights + self.corridor = corridor + self.portfolio_value = Decimal("0") + self.instrument_cache = {} + self.dry_run = dry_run + + def fetch_portfolio(self): + account = self.client.operations.get_portfolio(account_id=self.account_id) + self.portfolio_value = to_dec(account.total_amount_portfolio) + + return account.positions + + def get_instrument_data(self, instrument_uid): + if instrument_uid not in self.instrument_cache: + resp = self.client.instruments.find_instrument(query=instrument_uid) + self.instrument_cache[instrument_uid] = resp.instruments[0] + + return self.instrument_cache[instrument_uid] + + def calculate_rebalance(self, positions, target_config) -> tuple[list[dict], str]: + plan = [] + plan_report = "\n📊 План ребаланса\n" + allowed_uids = {info["uid"] for info in target_config.values()} + current_values = {} + current_prices = {} + + for pos in positions: + # Пропускаем кэш (рубли) только при проверке лишних активов, + # т.к. кэш учитывается в target_config как RUB + if pos.instrument_type == "currency": + continue + + if pos.instrument_uid not in allowed_uids: + ticker = pos.ticker + qty = to_dec(pos.quantity) + + if qty > 0: + # Получаем данные об инструменте для определения лотности + instr_info = self.get_instrument_data(pos.instrument_uid) + # В Тинькофф продажа идет в лотах + lots_to_sell = int(qty / instr_info.lot) + + if lots_to_sell > 0: + print( + f"🗑️ ЛИШНИЙ АКТИВ: {ticker} (нет в конфиге). Продаем {lots_to_sell} лотов." + ) + plan_report += f"• SELL {ticker}: {lots_to_sell} лотов (не в конфиге)\n" + plan.append( + { + "ticker": ticker, + "figi": pos.figi, + "action": "SELL", + "lots": lots_to_sell, + "reason": "not_in_config", + } + ) + + current_map = {pos.instrument_uid: pos for pos in positions} + + for ticker, info in target_config.items(): + uid = info["uid"] + target_weight = info["weight"] + + pos = current_map.get(uid) + if pos is None: + current_weight = Decimal("0") + current_value = Decimal("0") + else: + price = to_dec(pos.current_price) + qty = to_dec(pos.quantity) + current_value = price * qty + current_weight = current_value / self.portfolio_value + if ticker != "RUB": + current_prices[ticker] = price + + current_values[ticker] = current_value + + delta = target_weight - current_weight + money_delta = delta * self.portfolio_value + + print( + f"{ticker}, Текущая доля {current_weight:.2%}, Цель {target_weight:.2%}, Дельта: {delta:.2%}, Дельта в рублях: {money_delta:.6}" + ) + + if abs(delta) > self.corridor: + # Для RUB не создаём ордера, только выводим информацию + if ticker == "RUB": + if delta > 0: + plan_report += f"• ⚠️ Недостаток кэша: нужно {abs(delta * self.portfolio_value):.2f} руб. (уменьшите покупки)\n" + else: + plan_report += f"• 💰 Избыток кэша: {abs(delta * self.portfolio_value):.2f} руб. доступно для инвестиций\n" + else: + money_to_trade = self.portfolio_value * delta + instrument_info = self.get_instrument_data(uid) + one_lot_price = price * instrument_info.lot + lots = int(money_to_trade / one_lot_price) + + if lots != 0: + plan.append( + { + "figi": instrument_info.figi, + "ticker": ticker, + "uid": uid, + "action": "BUY" if lots > 0 else "SELL", + "lots": abs(lots), + "delta_pct": delta * 100, + } + ) + plan_report += f"• {'BUY' if lots > 0 else 'SELL'} {ticker}: {abs(lots)} лотов; на сумму {money_to_trade:.2f} руб.\n" + + print( + f"{ticker:12} | Доля: {current_weight:6.2%} | Цель: {target_weight:6.2%}" + ) + + # Инвестируем избыток кэша, даже если остальные отклонения в пределах коридора + if "RUB" in target_config and "RUB" in current_values: + target_cash = self.portfolio_value * target_config["RUB"]["weight"] + cash_excess = current_values["RUB"] - target_cash + + if cash_excess > 0: + plan_report += f"• 💰 Избыток кэша: {cash_excess:.2f} руб. будет распределён по целям\n" + non_rub_weights = { + t: info["weight"] + for t, info in target_config.items() + if t != "RUB" + } + total_non_rub_weight = sum(non_rub_weights.values()) + + if total_non_rub_weight > 0: + for ticker, weight in non_rub_weights.items(): + uid = target_config[ticker]["uid"] + share = weight / total_non_rub_weight + money_to_trade = cash_excess * share + + price = current_prices.get(ticker) + if price is None or price <= 0: + plan_report += f"• ⚠️ Пропуск {ticker}: нет цены для расчёта лотов\n" + continue + + instrument_info = self.get_instrument_data(uid) + one_lot_price = price * instrument_info.lot + lots = int(money_to_trade / one_lot_price) + + if lots > 0: + plan.append( + { + "figi": instrument_info.figi, + "ticker": ticker, + "uid": uid, + "action": "BUY", + "lots": lots, + "delta_pct": Decimal("0"), + "reason": "cash_excess", + } + ) + plan_report += f"• BUY {ticker}: {lots} лотов; на сумму {money_to_trade:.2f} руб. (излишек кэша)\n" + plan_report += "\n— Конец плана ребаланса —\n" + + return plan, plan_report + + def execute_orders(self, trades, group_name) -> str: + + execute_report = f"\n⚙️ Исполнение: {group_name}\n" + + if not trades: + execute_report += "Нет сделок для исполнения.\n" + return execute_report + + print(f"\n--- Исполнение блока: {group_name} ---") + + for trade in trades: + action_ru = "КУПИТЬ" if trade["action"] == "BUY" else "ПРОДАТЬ" + + if self.dry_run: + execute_report += f"• Симуляция: {action_ru} {trade['ticker']} — {trade['lots']} лотов\n" + print( + f"[СИМУЛЯЦИЯ] {action_ru} {trade['ticker']}: {trade['lots']} лотов" + ) + continue + + try: + direction = ( + OrderDirection.ORDER_DIRECTION_BUY + if trade["action"] == "BUY" + else OrderDirection.ORDER_DIRECTION_SELL + ) + + response = self.client.orders.post_order( + figi=trade["figi"], + quantity=int(trade["lots"]), + direction=direction, + account_id=self.account_id, + order_type=OrderType.ORDER_TYPE_MARKET, + order_id=str(uuid.uuid4()), + ) + execute_report += f"• ✅ {action_ru} {trade['ticker']} — {trade['lots']} лотов. ID: {response.order_id}\n" + print( + f"[ИСПОЛНЕНО]\n{trade['ticker']} на {trade['lots']} лотов. ID: {response.order_id}" + ) + except Exception as e: + execute_report += f"• ❌ Ошибка по {trade['ticker']}: {e}\n" + print(f"[ОШИБКА]\nПри сделке с {trade['ticker']}: {e}") + + return execute_report diff --git a/config/target.yaml b/config/target.yaml new file mode 100644 index 0000000..219a120 --- /dev/null +++ b/config/target.yaml @@ -0,0 +1,14 @@ +portfolio: + TMOS@: + name: "Крупнейшие компании РФ" + figi: "TCS60A101X76" + uid: "f509af83-6e71-462f-901f-bcb073f6773b" + weight: 0.98 + + RUB: + name: "Рублевый кэш" + figi: "RUB000UTSTOM" + uid: "a92e2e25-a698-45cc-a781-167cf465257c" + weight: 0.02 + +corridor: 0.02 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d1660eb --- /dev/null +++ b/main.py @@ -0,0 +1,62 @@ +import os +from dotenv import load_dotenv +from t_tech.invest import Client +from t_tech.invest.utils import quotation_to_decimal as to_dec + +from app.rebalance import RebalanceBot +from app.load_config import load_config +from app.notifier import Notifier + +load_dotenv() + + +def main(): + report = "🚀 Запуск бота ребаланса портфеля\n" + tg = Notifier(token=os.getenv("TELEGRAM_BOT_TOKEN")) # type: ignore + config, corridor = load_config() + with Client(token=os.getenv("TINVEST_TOKEN")) as client: # type: ignore + bot = RebalanceBot( + client=client, + account_id=os.getenv("TINVEST_ACCOUNT_ID"), # type: ignore + target_weights=config, # type: ignore + corridor=corridor, # type: ignore + dry_run=os.getenv("DRY_RUN_OVERRIDE", "false").lower() == "true", + ) + + pos = bot.fetch_portfolio() + print(pos) + plan, plan_report = bot.calculate_rebalance(pos, bot.target_weights) + report += plan_report + + report += "\n——————————————\n" + + sales = [trade for trade in plan if trade["action"] == "SELL"] + buys = [trade for trade in plan if trade["action"] == "BUY"] + + sales_report = bot.execute_orders(sales, "ПРОДАЖИ") + report += sales_report + report += "\n——————————————\n" + + buys_report = bot.execute_orders(buys, "ПОКУПКИ") + report += buys_report + report += "\n——————————————\n" + + pos_after = bot.fetch_portfolio() + cash = next( + ( + p + for p in pos_after + if p.instrument_type == "currency" and p.ticker == "RUB000UTSTOM" + ), + None, + ) + cash_amount = to_dec(cash.quantity) # type: ignore + report += f"\n💰 Остаток кэша после ребаланса: {cash_amount:.2f} RUB\n" + + tg.send_message(user_id=int(os.getenv("TELEGRAM_USER_ID")), text=report) # type: ignore + + +if __name__ == "__main__": + main() + # with Client(token=os.getenv("TINVEST_TOKEN")) as client: + # print(client.users.get_accounts()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2f896ad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "t-tech-gyro" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "dotenv>=0.9.9", + "pyyaml>=6.0.3", + "requests>=2.32.5", + "t-tech-investments>=0.3.3", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cd53f3c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +t-tech-investments +pyyaml +dotenv +requests \ No newline at end of file diff --git a/tools/get_instrument.py b/tools/get_instrument.py new file mode 100644 index 0000000..2395f69 --- /dev/null +++ b/tools/get_instrument.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +import os +import sys +import logging +import argparse +from dotenv import load_dotenv + +logging.getLogger("t_tech.invest._grpc_helpers").setLevel(logging.ERROR) +from t_tech.invest import Client +from t_tech.invest.utils import quotation_to_decimal as to_dec + +load_dotenv() + + +def _account_type(t) -> str: + return {0: "неизвестно", 1: "Брокер", 2: "ИИС", 3: "Копилка", 4: "Фонд"}.get(t.value, f"тип:{t.value}") + + +def _account_status(s) -> str: + return {0: "неизвестно", 1: "новый", 2: "открыт", 3: "закрыт"}.get(s.value, f"статус:{s.value}") + + +def cmd_accounts(client, _args): + resp = client.users.get_accounts() + if not resp.accounts: + print("Счета не найдены.") + return + + print(f"\n{'ID':<45} {'Тип':<12} {'Статус':<10} Название") + print("─" * 90) + for acc in resp.accounts: + print(f"{acc.id:<45} {_account_type(acc.type):<12} {_account_status(acc.status):<10} {acc.name}") + print() + + +def cmd_search(client, args): + resp = client.instruments.find_instrument(query=args.query) + if not resp.instruments: + print(f"Ничего не найдено по запросу '{args.query}'") + return + + instruments = resp.instruments[: args.limit] + total = len(resp.instruments) + print(f"\nНайдено {total}, показано {len(instruments)}:\n") + print(f"{'Тикер':<14} {'Тип':<10} {'Лот':>5} {'Название':<42} UID") + print("─" * 110) + for i in instruments: + name = (i.name[:40] + "..") if len(i.name) > 40 else i.name + print(f"{i.ticker:<14} {i.instrument_type:<10} {i.lot:>5} {name:<42} {i.uid}") + print() + + +def cmd_yaml(client, args): + resp = client.instruments.find_instrument(query=args.query) + if not resp.instruments: + print(f"Ничего не найдено по запросу '{args.query}'") + return + + instruments = resp.instruments[: args.limit] + + if len(instruments) == 1: + instr = instruments[0] + else: + print(f"\nНайдено {len(resp.instruments)} результат(ов):\n") + for idx, i in enumerate(instruments): + name = (i.name[:42] + "..") if len(i.name) > 42 else i.name + print(f" [{idx}] {i.ticker:<14} {i.instrument_type:<10} {name}") + print() + try: + choice = int(input("Введите номер: ")) + instr = instruments[choice] + except (ValueError, IndexError): + print("Неверный выбор.") + return + + print(f"\n# Вставьте в config/target.yaml → portfolio:") + print(f" {instr.ticker}:") + print(f' name: "{instr.name}"') + print(f' figi: "{instr.figi}"') + print(f' uid: "{instr.uid}"') + print(f' weight: 0.00 # TODO: укажите целевой вес') + print() + + +def cmd_portfolio(client, args): + account_id = args.account_id or os.getenv("TINVEST_ACCOUNT_ID") + if not account_id: + print("TINVEST_ACCOUNT_ID не задан. Используйте --account или добавьте в .env") + print("Подсказка: запустите 'accounts', чтобы узнать ID счёта.") + sys.exit(1) + + resp = client.operations.get_portfolio(account_id=account_id) + total = to_dec(resp.total_amount_portfolio) + + positions = sorted( + resp.positions, + key=lambda p: to_dec(p.current_price) * to_dec(p.quantity), + reverse=True, + ) + + print(f"\nСтоимость портфеля: {total:.2f} RUB\n") + print(f"{'Тикер':<14} {'Тип':<12} {'Кол-во':>10} {'Цена':>12} {'Сумма':>14} {'Доля':>7} UID") + print("─" * 115) + for pos in positions: + qty = to_dec(pos.quantity) + price = to_dec(pos.current_price) + value = qty * price + share = float(value / total * 100) if total else 0 + print( + f"{pos.ticker:<14} {pos.instrument_type:<12} {qty:>10.2f} " + f"{price:>12.4f} {value:>14.2f} {share:>6.2f}% {pos.instrument_uid}" + ) + print() + + +def main(): + parser = argparse.ArgumentParser( + description="Утилита поиска инструментов T-Invest", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +примеры: + python tools/get_instrument.py accounts + python tools/get_instrument.py search TMOS@ + python tools/get_instrument.py search "Тинькофф" -n 5 + python tools/get_instrument.py yaml TMOS@ + python tools/get_instrument.py portfolio + python tools/get_instrument.py portfolio --account +""", + ) + parser.add_argument("--token", help="Токен T-Invest (переопределяет TINVEST_TOKEN из .env)") + sub = parser.add_subparsers(dest="cmd", required=True) + + sub.add_parser("accounts", help="Список счетов и их ID") + + p_search = sub.add_parser("search", help="Поиск инструментов по тикеру, ISIN или названию") + p_search.add_argument("query", help="Тикер, ISIN или часть названия") + p_search.add_argument("-n", "--limit", type=int, default=10, metavar="N", help="Макс. результатов (по умолчанию: 10)") + + p_yaml = sub.add_parser("yaml", help="Сгенерировать сниппет для config/target.yaml") + p_yaml.add_argument("query", help="Тикер, ISIN или часть названия") + p_yaml.add_argument("-n", "--limit", type=int, default=8, metavar="N", help="Макс. результатов для выбора (по умолчанию: 8)") + + p_port = sub.add_parser("portfolio", help="Текущие позиции портфеля") + p_port.add_argument("--account", dest="account_id", metavar="ID", help="ID счёта (переопределяет TINVEST_ACCOUNT_ID из .env)") + + args = parser.parse_args() + + token = args.token or os.getenv("TINVEST_TOKEN") + if not token: + print("Ошибка: TINVEST_TOKEN не задан. Передайте --token или добавьте в .env") + sys.exit(1) + + with Client(token=token) as client: + {"accounts": cmd_accounts, "search": cmd_search, "yaml": cmd_yaml, "portfolio": cmd_portfolio}[args.cmd](client, args) + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fc906b6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,296 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "protobuf" +version = "4.25.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920, upload-time = "2025-05-28T14:22:25.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745, upload-time = "2025-05-28T14:22:10.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736, upload-time = "2025-05-28T14:22:13.156Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537, upload-time = "2025-05-28T14:22:14.768Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005, upload-time = "2025-05-28T14:22:16.052Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924, upload-time = "2025-05-28T14:22:17.105Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f0/0e9dc590513d5e742d7799e2038df3a05167cba084c6ca4f3cdd75b55164/sentry_sdk-2.48.0.tar.gz", hash = "sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473", size = 384828, upload-time = "2025-12-16T14:55:41.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/19/8d77f9992e5cbfcaa9133c3bf63b4fbbb051248802e1e803fed5c552fbb2/sentry_sdk-2.48.0-py2.py3-none-any.whl", hash = "sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172", size = 414555, upload-time = "2025-12-16T14:55:40.152Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "t-tech-gyro" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "t-tech-investments" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "t-tech-investments", specifier = ">=0.3.3" }, +] + +[[package]] +name = "t-tech-investments" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "deprecation" }, + { name = "grpcio" }, + { name = "protobuf" }, + { name = "python-dateutil" }, + { name = "sentry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/1d/7744d4cded9416725e22f94352fc8080527d2f6924a26690b49c691d8c24/t_tech_investments-0.3.3.tar.gz", hash = "sha256:0657271e79c518bb5796614eea5a940587d66fee5db0eeb8a377a2a3c047820b", size = 227231, upload-time = "2025-12-30T06:55:19.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/41/ca4f7b8985c74035744313af8af999d82e5793f8f3fc676b7580dadc9653/t_tech_investments-0.3.3-py3-none-any.whl", hash = "sha256:cdcce4e608d43e63b8dd73765dc831cb1022f64eb7ea7e8e53968b1b280aafcd", size = 260460, upload-time = "2025-12-30T06:55:17.6Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +]