fix: использовать requests.Session для переиспользования соединений (ISSUE #8)

This commit is contained in:
deadzilla 2026-05-26 18:17:17 +05:00
parent 4425f27e1e
commit 5292f9bd0e
5 changed files with 24 additions and 42 deletions

View File

@ -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 мм рт. ст.
```
## Безопасность

View File

@ -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()`.
---

View File

@ -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("Не удалось получить котика. Попробуйте позже.")

View File

@ -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

View File

@ -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", {})