diff --git a/AGENTS.md b/AGENTS.md index 0743129..dafafb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,18 +1,13 @@ # AGENTS.md ## Проект -Discord-бот на Python (discord.py) для Магнитогорска. -- Команды Discord: `!msg` (повтор текста), `!pogoda` (прогноз погоды) -- Консольные команды: `stop` (остановка бота) +Discord-бот на Python (discord.py) ## Запуск ```bash pip install -r requirements.txt python bot.py ``` - -Введите `stop` в терминал для остановки бота. - ## Токен Токен бота хранится в `.env` (переменная `DISCORD_TOKEN`). Получи токен на [Discord Developer Portal](https://discord.com/developers/applications). Шаблон в `.env.example`. @@ -25,27 +20,6 @@ python bot.py - **Все git-коммиты согласовывать с пользователем перед созданием.** - **Все сообщения git-коммитов писать на русском языке.** -## Архитектура -``` -bot.py # Точка входа, инициализация бота, console_input() -commands/ # Discord команды (cogs) - __init__.py # ALL_COMMANDS — явные импорты - pogoda.py # !pogoda — погода с retry + fallback (wttr.in → Open-Meteo) -console_commands/ # Консольные команды - __init__.py # ALL_CONSOLE_COMMANDS — явные импорты - stop.py # stop — остановка бота -``` - -### Добавление Discord команды -1. Создать файл `commands/имя.py` с классом, наследующим `commands.Cog` -2. Добавить импорт в `commands/__init__.py` -3. Добавить класс в `ALL_COMMANDS` - -### Добавление консольной команды -1. Создать файл `console_commands/имя.py` с функцией `func(stop_event, bot)` -2. Добавить импорт в `console_commands/__init__.py` -3. Добавить функцию в `ALL_CONSOLE_COMMANDS` - ## API и внешние сервисы ### Погода (!pogoda) @@ -75,11 +49,11 @@ requests>=2.31.0 ## Структура данных погоды Команда `!pogoda` возвращает: ``` -[TEMP] Температура: X°C (ощущается как Y°C) -[DESC] Описание: Z -[HUMID] Влажность: X% -[WIND] Ветер: X м/с -[PRESS] Давление: X мм рт. ст. +Температура: X°C (ощущается как Y°C) +Описание: Z +Влажность: X% +Ветер: X м/с +Давление: X мм рт. ст. ``` ## Безопасность diff --git a/ISSUES.md b/ISSUES.md index e24e0c3..b8bccba 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -53,10 +53,11 @@ - **Решение:** Создать `.gitignore` с правилами для Python (`__pycache__/`, `*.pyc`, `.env`, `venv/`, `*.egg-info/`), логов, временных файлов, кэша ОС и IDE. - **Статус:** Исправлено. `.gitignore` создан и обновлён. -### 8. `requests` без Session — нет переиспользования соединений -- **Где:** все файлы (`pogoda.py`, `news.py`) +### 8. `requests` без Session — нет переиспользования соединений ✅ РЕШЕНО +- **Где:** `utils/pogoda.py`, `utils/news.py`, `commands/cat.py` - **Проблема:** Каждый `requests.get()` создаёт новое TCP-соединение. Для частых запросов это расточительно. - **Решение:** Создать `requests.Session()` и использовать `session.get()`. +- **Статус:** Исправлено. Session создан в каждом модуле, все `requests.get()` заменены на `_session.get()`. --- diff --git a/commands/cat.py b/commands/cat.py index 87efff8..383dc19 100644 --- a/commands/cat.py +++ b/commands/cat.py @@ -3,6 +3,8 @@ from discord.ext import commands import asyncio import requests +_session = requests.Session() + class Cat(commands.Cog): """Команда !cat — случайный котик""" @@ -12,7 +14,7 @@ class Cat(commands.Cog): """Получить случайного котика""" try: response = await asyncio.to_thread( - requests.get, + _session.get, "https://api.thecatapi.com/v1/images/search", timeout=10 ) @@ -26,5 +28,5 @@ class Cat(commands.Cog): ) embed.set_image(url=url) await ctx.send(embed=embed) - except requests.RequestException: + except requests.exceptions.RequestException: await ctx.send("Не удалось получить котика. Попробуйте позже.") diff --git a/utils/news.py b/utils/news.py index 71a7f36..1b9f554 100644 --- a/utils/news.py +++ b/utils/news.py @@ -1,17 +1,20 @@ +import asyncio from datetime import datetime +import requests + RSS_URL_ARTICLES = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru" RSS_URL_POSTS = "https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru" +_session = requests.Session() + async def fetch_rss(url): """Скачать и распарсить RSS-ленту (RSS 2.0 / Atom).""" - import asyncio - import requests from xml.etree import ElementTree try: - response = await asyncio.to_thread(requests.get, url, timeout=10) + response = await asyncio.to_thread(_session.get, url, timeout=10) response.raise_for_status() root = ElementTree.fromstring(response.content) @@ -60,7 +63,7 @@ async def fetch_rss(url): "tags": tags, }) return articles[:10] - except requests.RequestException: + except requests.exceptions.RequestException: return None diff --git a/utils/pogoda.py b/utils/pogoda.py index 33fd56f..020614a 100644 --- a/utils/pogoda.py +++ b/utils/pogoda.py @@ -2,12 +2,14 @@ import asyncio import requests from requests.exceptions import ConnectionError, Timeout, SSLError +_session = requests.Session() + async def fetch_weather(api_url, timeout=10, max_retries=3): """Получить данные о погоде с retry.""" for attempt in range(max_retries): try: - response = await asyncio.to_thread(requests.get, api_url, timeout=timeout) + response = await asyncio.to_thread(_session.get, api_url, timeout=timeout) response.raise_for_status() return response.json() except (SSLError, ConnectionError, Timeout): @@ -35,7 +37,7 @@ async def fetch_open_meteo(lat=53.4069, lon=58.9797, timeout=10, max_retries=3): ) for attempt in range(max_retries): try: - response = await asyncio.to_thread(requests.get, url, timeout=timeout) + response = await asyncio.to_thread(_session.get, url, timeout=timeout) response.raise_for_status() data = response.json() current = data.get("current", {})