Compare commits

..
10 Commits
Author SHA1 Message Date
Leo b59c3c1a55 Fix CI/CD
My first CI/CD Pipeline / Formatting Check (push) Canceled after 0s
My first CI/CD Pipeline / Security Scan (push) Canceled after 0s
My first CI/CD Pipeline / Build and push to Docker Hub (push) Canceled after 0s
My first CI/CD Pipeline / Deploy to VPS via SSH (push) Canceled after 0s
2026-07-10 15:14:58 +05:00
Leo 2157ccd9ec Add CD 2026-07-10 15:01:38 +05:00
Leo becb16f282 Исправил форматирование кода по PEP8 2026-07-09 12:59:10 +05:00
Leo d0f08abed7 Update CI 2026-07-09 12:40:17 +05:00
Leo 068497e9f0 Fix CI 2026-07-09 09:52:11 +05:00
Leo 7f6c8095e3 Add test CI 2026-07-09 09:44:57 +05:00
Leo 9037b94067 Fix CI 2026-07-09 09:29:59 +05:00
Leo 48fb140bc4 Merge README 2026-07-09 09:23:45 +05:00
Leo 7bc2b84294 Add CI 2026-07-09 09:20:55 +05:00
LeoR156 13c4091f38 Update README.md 2026-07-04 18:17:39 +05:00
9 changed files with 396 additions and 133 deletions
+78
View File
@@ -0,0 +1,78 @@
name: My first CI/CD Pipeline
on:
push:
branches:
- main
jobs:
formatting-check:
name: Formatting Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: run YAPF to test if python code is correctly formatted
uses: AlexanderMelde/yapf-action@v2.0
with:
args: --verbose
security-scan:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run OWASP Noir
id: noir
uses: owasp-noir/noir@v1.1.0
with:
base_path: '.'
- name: Display results
run: echo '${{ steps.noir.outputs.endpoints }}' | jq .
build-and-push:
name: Build and push to Docker Hub
needs: [security-scan, formatting-check]
runs-on: ubuntu-latest
steps:
- name: Скачиваем код бота
uses: actions/checkout@v4
- name: Логинимся в Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.docker_login }}
password: ${{ secrets.docker_pass }}
- name: Сборка и пуш Docker образа
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: leor156/pogoda-api-tgbot:latest
deploy:
name: Deploy to VPS via SSH
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Подключение по SSH и обновление контейнера
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
docker pull leor156/pogoda-api-tgbot:latest
docker stop weather_bot || true
docker rm weather_bot || true
docker run -d \
--name weather_bot \
--restart unless-stopped \
-e TG_TOKEN="${{ secrets.TG_TOKEN }}" \
-e WEATHER_API_KEY="${{ secrets.WEATHER_API_KEY }}" \
leor156/pogoda-api-tgbot:latest
docker image prune -f
+47 -7
View File
@@ -1,8 +1,48 @@
# ⛅ Weather Telegram Bot (with Ansible Deployment)
Бот для получения погоды, упакованный в Docker. В репозитории также находится Ansible-плейбук для автоматического развертывания на любом облачном сервере Ubuntu за 2 минуты.
## 🚀 Быстрый старт (Локально через Docker)
1. Склонируй репозиторий.
2. Создай файл `.env` и добавь туда переменные `TG_TOKEN` и `WEATHER_API_KEY`.
3. Запусти: `docker-compose up -d`.
# 🌤️ Weather Telegram Bot (with Ansible Deployment)
# Инструкция написана не до конца. Более подробная версия выйдет позже
Бот для получения прогноза погоды, упакованный в Docker. В репозитории также находится Ansible-плейбук для автоматического развертывания на любом облачном сервере Ubuntu за 2 минуты.
## 🚀 Быстрый старт за 2 минуты
1. **Склонируйте репозиторий** командой:
```bash
git clone https://github.com/LeoR156/pogoda_tg_bot.git
cd pogoda_tg_bot
```
2. **Получите токены и ключи**:
- Создайте бота(ов) и скопируйте токен(ы) у [@BotFather](https://t.me/BotFather) в Telegram.
- Получите бесплатный API ключ на сайте OpenWeatherMap.
- Узнайте свой Telegram ID у официального бота [@userinfobot](https://t.me/userinfobot)
3. **Создайте файл инвентори**:
Создайте файл `inventory.ini` и добавьте туда серверы для установки. Вы можете задать общие переменные `admin` (ваш ID) и `api_pogoda` (ключ погоды) для всех серверов. А вот токен телеграм-бота `tg_token` **должен быть обязательно разным для каждого бота**, иначе боты будут конфликтовать за входящие сообщения.
*Пример синтаксиса `inventory.ini`:*
```ini
[tg_bot_3x]
# Указываем IP серверов и уникальные токены ботов для каждого сервера
server1 ansible_host=222.222.222.222 tg_token="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
server2 ansible_host=111.111.111.111 tg_token="987654:XYZ-ABC1234ghIkl-zyx57W2v1u123ew11"
[tg_bot_3x:vars]
# Общие переменные для всех хостов в этой группе
api_pogoda="ваш_ключ_openweathermap"
admin=123456789
```
*(Примечание: файл `.env` вам не нужен, так как Ansible сам пробросит переменные из inventory прямо в Docker-контейнеры!)*
4. **Настройте Playbook**:
В файле `playbook.yml` убедитесь, что в поле `hosts:` указана группа из вашего `inventory.ini` (в примере выше это `tg_bot_3x`).
5. **Запуск**:
Проверьте файл на ошибки командой:
```bash
ansible-playbook playbook.yml --syntax-check
```
Если есть ошибки, то исправьте синтаксис в файлу. Если всё хорошо, запускайте автоматическую установку командой:
```bash
ansible-playbook playbook.yml -i inventory.ini
```
Через несколько минут Ansible закончит свою работу и вы получите работающих телеграмм ботов!
+63 -30
View File
@@ -1,57 +1,90 @@
import sqlite3
import os
import psycopg2
from psycopg2.extras import RealDictCursor
import logging
from datetime import datetime
from contextlib import contextmanager
DB_PATH = 'bot_database.db'
logger = logging.getLogger(__name__)
# Читаем параметры подключения из окружения (или используем дефолтные для локальной разработки)
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "postgres")
DB_USER = os.getenv("DB_USER", "postgres")
DB_PASS = os.getenv("DB_PASS", "")
@contextmanager
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
# Используем RealDictCursor, чтобы код в tg.py мог обращаться к полям как к словарю: user['default_city']
conn = psycopg2.connect(host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS,
cursor_factory=RealDictCursor)
try:
yield conn
finally:
conn.commit()
conn.close()
def init_db():
with get_db_connection() as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id INTEGER UNIQUE NOT NULL,
username TEXT,
default_city TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
logger.info("База данных инициализирована.")
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
# В PostgreSQL используем SERIAL для автоинкремента
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
telegram_id BIGINT UNIQUE NOT NULL,
username TEXT,
default_city TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
logger.info("База данных PostgreSQL успешно инициализирована.")
except Exception as e:
logger.error(f"Ошибка при инициализации PostgreSQL: {e}")
# Если база обязательна для запуска, можно сделать raise e, чтобы бот не стартовал впустую
raise e
def get_user(telegram_id):
with get_db_connection() as conn:
cursor = conn.execute('SELECT * FROM users WHERE telegram_id = ?', (telegram_id,))
return cursor.fetchone()
with conn.cursor() as cursor:
cursor.execute('SELECT * FROM users WHERE telegram_id = %s',
(telegram_id, ))
return cursor.fetchone()
def add_user(telegram_id, username):
with get_db_connection() as conn:
try:
conn.execute(
'INSERT OR IGNORE INTO users (telegram_id, username) VALUES (?, ?)',
(telegram_id, username)
)
except Exception as e:
logger.error(f"Ошибка при добавлении пользователя: {e}")
with conn.cursor() as cursor:
try:
# В PostgreSQL синтаксис избегания дубликатов - ON CONFLICT DO NOTHING
cursor.execute(
'INSERT INTO users (telegram_id, username) VALUES (%s, %s) ON CONFLICT (telegram_id) DO NOTHING',
(telegram_id, username))
except Exception as e:
logger.error(f"Ошибка при добавлении пользователя: {e}")
def update_default_city(telegram_id, city):
with get_db_connection() as conn:
conn.execute(
'UPDATE users SET default_city = ? WHERE telegram_id = ?',
(city, telegram_id)
)
with conn.cursor() as cursor:
cursor.execute(
'UPDATE users SET default_city = %s WHERE telegram_id = %s',
(city, telegram_id))
def get_users_count():
with get_db_connection() as conn:
cursor = conn.execute('SELECT COUNT(*) FROM users')
return cursor.fetchone()[0]
with conn.cursor() as cursor:
cursor.execute('SELECT COUNT(*) FROM users')
# Так как мы используем RealDictCursor, результат будет {'count': X}
# Но для COUNT(*) безопаснее забрать по индексу или ключу
res = cursor.fetchone()
return res['count'] if res else 0
+13 -13
View File
@@ -1,32 +1,32 @@
from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
def get_main_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
markup.add(
KeyboardButton("Текущая погода"),
KeyboardButton("Прогноз на 3 дня"),
KeyboardButton("Прогноз на 5 дней"),
KeyboardButton("Настройки")
)
markup.add(KeyboardButton("Текущая погода"),
KeyboardButton("Прогноз на 3 дня"),
KeyboardButton("Прогноз на 5 дней"),
KeyboardButton("Настройки"))
return markup
def get_settings_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
markup.add(
KeyboardButton("Изменить город"),
KeyboardButton("🔙 Назад в меню")
)
markup.add(KeyboardButton("Изменить город"),
KeyboardButton("🔙 Назад в меню"))
return markup
def get_cancel_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
markup.add(KeyboardButton("Отмена"))
return markup
def get_city_confirm_inline(city_name):
markup = InlineKeyboardMarkup()
markup.add(
InlineKeyboardButton("Да, сохранить", callback_data=f"save_city:{city_name}"),
InlineKeyboardButton("Нет, отмена", callback_data="cancel_city")
)
InlineKeyboardButton("Да, сохранить",
callback_data=f"save_city:{city_name}"),
InlineKeyboardButton("Нет, отмена", callback_data="cancel_city"))
return markup
+4
View File
@@ -1,2 +1,6 @@
pyTelegramBotAPI==4.14.0
requests==2.31.0
psycopg2-binary==2.9.9
Flask==3.0.3
gunicorn==22.0.0
pytest
+17
View File
@@ -0,0 +1,17 @@
# test_bot.py
from weather_utils import get_weather_comment
def test_weather_comment_hot():
# Проверяем, что при +30 будет "Жара! ☀️"
assert get_weather_comment(30) == "Жара! ☀️"
def test_weather_comment_cold():
# Проверяем, что при -10 будет "Дубак! ❄️"
assert get_weather_comment(-10) == "Дубак! ❄️"
def test_weather_comment_normal():
# Проверяем промежуточное значение
assert get_weather_comment(15) == "Нормальная погода ☁️"
+109 -48
View File
@@ -1,6 +1,8 @@
import os
import time
import telebot
import logging
from flask import Flask, request, abort
import db
import keyboards as kb
@@ -9,31 +11,47 @@ import weather_api as wa
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Инициализация переменных окружения
TG_TOKEN = os.getenv("TG_TOKEN")
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
ADMIN_ID = os.getenv("ADMIN_ID")
WEBHOOK_HOST = os.getenv("WEBHOOK_HOST") # e.g., https://my-bot.com
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8080))
if not TG_TOKEN or not WEATHER_API_KEY:
logger.error("Не заданы переменные окружения TG_TOKEN или WEATHER_API_KEY!")
logger.error(
"Не заданы переменные окружения TG_TOKEN или WEATHER_API_KEY!")
exit(1)
if not WEBHOOK_HOST:
logger.error(
"Не задана переменная WEBHOOK_HOST! (например: https://mydomain.com)")
exit(1)
# Формируем URL вебхука (скрываем токен в пути для безопасности)
WEBHOOK_PATH = f"/{TG_TOKEN}/"
WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
# Инициализация БД
db.init_db()
# Инициализация бота
bot = telebot.TeleBot(TG_TOKEN)
# Инициализация Flask приложения
app = Flask(__name__)
def get_user_city(user_id):
user = db.get_user(user_id)
if user and user['default_city']:
return user['default_city']
return None
@bot.message_handler(commands=['start', 'help'])
def send_welcome(message):
user_id = message.from_user.id
@@ -45,26 +63,31 @@ def send_welcome(message):
welcome_text = (
"Привет! 🌍 Я твой погодный бот.\n\n"
"Я могу показывать текущую погоду и прогноз на несколько дней.\n"
"Для удобства давай установим твой город по умолчанию."
)
"Для удобства давай установим твой город по умолчанию.")
user_city = get_user_city(user_id)
if user_city:
welcome_text += f"\n\nТвой текущий город: <b>{user_city}</b>"
bot.send_message(message.chat.id, welcome_text, reply_markup=kb.get_main_menu(), parse_mode="HTML")
bot.send_message(message.chat.id,
welcome_text,
reply_markup=kb.get_main_menu(),
parse_mode="HTML")
else:
bot.send_message(message.chat.id, welcome_text, parse_mode="HTML")
msg = bot.send_message(message.chat.id, "Напиши название города, который хочешь установить по умолчанию:", reply_markup=kb.get_cancel_menu())
msg = bot.send_message(
message.chat.id,
"Напиши название города, который хочешь установить по умолчанию:",
reply_markup=kb.get_cancel_menu())
bot.register_next_step_handler(msg, process_city_input)
@bot.message_handler(commands=['stats'])
def admin_stats(message):
if ADMIN_ID and str(message.from_user.id) == str(ADMIN_ID):
count = db.get_users_count()
bot.send_message(message.chat.id, f"📊 Уникальных пользователей в базе: {count}")
else:
# Игнорируем или выдаем сообщение о недостатке прав
pass
bot.send_message(message.chat.id,
f"📊 Уникальных пользователей в базе: {count}")
@bot.message_handler(func=lambda message: message.text == "Настройки")
def settings_menu(message):
@@ -74,78 +97,93 @@ def settings_menu(message):
text += f"Текущий город: <b>{user_city}</b>"
else:
text += "Город по умолчанию не установлен."
bot.send_message(message.chat.id, text, reply_markup=kb.get_settings_menu(), parse_mode="HTML")
bot.send_message(message.chat.id,
text,
reply_markup=kb.get_settings_menu(),
parse_mode="HTML")
@bot.message_handler(func=lambda message: message.text == "🔙 Назад в меню")
def back_to_main(message):
bot.send_message(message.chat.id, "Возвращаемся в главное меню.", reply_markup=kb.get_main_menu())
bot.send_message(message.chat.id,
"Возвращаемся в главное меню.",
reply_markup=kb.get_main_menu())
@bot.message_handler(func=lambda message: message.text == "Изменить город")
def change_city_prompt(message):
msg = bot.send_message(message.chat.id, "Напиши название нового города:", reply_markup=kb.get_cancel_menu())
msg = bot.send_message(message.chat.id,
"Напиши название нового города:",
reply_markup=kb.get_cancel_menu())
bot.register_next_step_handler(msg, process_city_input)
def process_city_input(message):
if message.text == "Отмена":
bot.send_message(message.chat.id, "Действие отменено.", reply_markup=kb.get_main_menu())
bot.send_message(message.chat.id,
"Действие отменено.",
reply_markup=kb.get_main_menu())
return
city = message.text.strip()
# Делаем проверочный запрос к API
weather_data = wa.get_current_weather(city)
if weather_data["success"]:
real_city_name = weather_data["city"]
text = (
f"Город <b>{real_city_name}</b> найден!\n"
f"Текущая температура: {weather_data['temp']}°C\n\n"
f"Сохранить этот город как основной?"
)
text = (f"Город <b>{real_city_name}</b> найден!\n"
f"Текущая температура: {weather_data['temp']}°C\n\n"
f"Сохранить этот город как основной?")
bot.send_message(
message.chat.id,
text,
reply_markup=kb.get_city_confirm_inline(real_city_name),
parse_mode="HTML"
)
parse_mode="HTML")
else:
msg = bot.send_message(
message.chat.id,
f"❌ {weather_data['error']}\nПопробуй ввести название еще раз или нажми 'Отмена'.",
reply_markup=kb.get_cancel_menu()
)
reply_markup=kb.get_cancel_menu())
bot.register_next_step_handler(msg, process_city_input)
@bot.callback_query_handler(func=lambda call: call.data.startswith('save_city:'))
@bot.callback_query_handler(
func=lambda call: call.data.startswith('save_city:'))
def callback_save_city(call):
city_name = call.data.split(':', 1)[1]
user_id = call.from_user.id
db.update_default_city(user_id, city_name)
bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
text=f"✅ Город <b>{city_name}</b> успешно сохранен!",
parse_mode="HTML"
)
bot.send_message(call.message.chat.id, "Открываю главное меню:", reply_markup=kb.get_main_menu())
bot.edit_message_text(chat_id=call.message.chat.id,
message_id=call.message.message_id,
text=f"✅ Город <b>{city_name}</b> успешно сохранен!",
parse_mode="HTML")
bot.send_message(call.message.chat.id,
"Открываю главное меню:",
reply_markup=kb.get_main_menu())
bot.answer_callback_query(call.id)
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_city')
def callback_cancel_city(call):
bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
text="❌ Изменение города отменено."
)
bot.send_message(call.message.chat.id, "Возвращаемся в главное меню:", reply_markup=kb.get_main_menu())
bot.edit_message_text(chat_id=call.message.chat.id,
message_id=call.message.message_id,
text="❌ Изменение города отменено.")
bot.send_message(call.message.chat.id,
"Возвращаемся в главное меню:",
reply_markup=kb.get_main_menu())
bot.answer_callback_query(call.id)
@bot.message_handler(func=lambda message: message.text in ["Текущая погода", "Прогноз на 3 дня", "Прогноз на 5 дней"])
@bot.message_handler(
func=lambda message: message.text in
["Текущая погода", "Прогноз на 3 дня", "Прогноз на 5 дней"])
def weather_requests(message):
user_city = get_user_city(message.from_user.id)
if not user_city:
msg = bot.send_message(message.chat.id, "Сначала укажите город. Напишите название:", reply_markup=kb.get_cancel_menu())
msg = bot.send_message(message.chat.id,
"Сначала укажите город. Напишите название:",
reply_markup=kb.get_cancel_menu())
bot.register_next_step_handler(msg, process_city_input)
return
@@ -174,9 +212,9 @@ def weather_requests(message):
else:
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
@bot.message_handler(func=lambda message: True)
def unknown_text(message):
# Если введен какой-то произвольный текст, предполагаем, что юзер хочет узнать погоду для этого города (разово)
city = message.text.strip()
bot.send_chat_action(message.chat.id, 'typing')
@@ -187,11 +225,34 @@ def unknown_text(message):
answer += f"На улице: {data['desc']}."
bot.send_message(message.chat.id, answer, parse_mode="HTML")
else:
bot.send_message(message.chat.id, f"❌ {data['error']}\nИспользуйте меню или введите корректное название города.", reply_markup=kb.get_main_menu())
bot.send_message(
message.chat.id,
f"❌ {data['error']}\nИспользуйте меню или введите корректное название города.",
reply_markup=kb.get_main_menu())
# --- ВЕБХУКИ (Flask) ---
@app.route(WEBHOOK_PATH, methods=['POST'])
def webhook():
if request.headers.get('content-type') == 'application/json':
json_string = request.get_data().decode('utf-8')
update = telebot.types.Update.de_json(json_string)
bot.process_new_updates([update])
return '', 200
else:
abort(403)
if __name__ == "__main__":
logger.info("Бот запущен и ждет сообщений...")
try:
bot.polling(none_stop=True)
except Exception as e:
logger.error(f"Критическая ошибка при поллинге: {e}")
logger.info("Удаляем старый вебхук...")
bot.remove_webhook()
time.sleep(1)
logger.info(f"Устанавливаем новый вебхук: {WEBHOOK_URL}")
bot.set_webhook(url=WEBHOOK_URL)
logger.info(f"Flask сервер запущен на порту {WEBHOOK_PORT}")
# Для продакшена лучше использовать Gunicorn, но для Docker часто достаточно и встроенного (или Waitress)
app.run(host='0.0.0.0', port=WEBHOOK_PORT)
+27 -8
View File
@@ -10,6 +10,7 @@ WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
BASE_URL = "https://api.openweathermap.org/data/2.5"
TIMEOUT = 10
def get_current_weather(city: str) -> dict:
url = f"{BASE_URL}/weather"
params = {
@@ -32,11 +33,19 @@ def get_current_weather(city: str) -> dict:
elif response.status_code == 404:
return {"success": False, "error": f"Город '{city}' не найден."}
else:
logger.error(f"API Error: Code {response.status_code} for city {city}")
return {"success": False, "error": "Ошибка связи с сервером погоды."}
logger.error(
f"API Error: Code {response.status_code} for city {city}")
return {
"success": False,
"error": "Ошибка связи с сервером погоды."
}
except requests.exceptions.RequestException as e:
logger.error(f"Request exception: {e}")
return {"success": False, "error": "Превышено время ожидания или ошибка сети."}
return {
"success": False,
"error": "Превышено время ожидания или ошибка сети."
}
def get_forecast(city: str, days: int) -> dict:
url = f"{BASE_URL}/forecast"
@@ -60,7 +69,8 @@ def get_forecast(city: str, days: int) -> dict:
# дата в формате 'YYYY-MM-DD HH:MM:SS'
date_str = item["dt_txt"].split(" ")[0]
daily_data[date_str]["temps"].append(item["main"]["temp"])
daily_data[date_str]["descs"].append(item["weather"][0]["description"])
daily_data[date_str]["descs"].append(
item["weather"][0]["description"])
# Сортируем дни
sorted_dates = sorted(daily_data.keys())
@@ -77,7 +87,8 @@ def get_forecast(city: str, days: int) -> dict:
max_temp = max(temps)
# Самое частое описание погоды за день
most_common_desc = max(set(descs), key=descs.count).capitalize()
most_common_desc = max(set(descs),
key=descs.count).capitalize()
# Перевод даты в читаемый формат
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
@@ -95,8 +106,16 @@ def get_forecast(city: str, days: int) -> dict:
elif response.status_code == 404:
return {"success": False, "error": f"Город '{city}' не найден."}
else:
logger.error(f"API Error (Forecast): Code {response.status_code} for city {city}")
return {"success": False, "error": "Ошибка связи с сервером погоды."}
logger.error(
f"API Error (Forecast): Code {response.status_code} for city {city}"
)
return {
"success": False,
"error": "Ошибка связи с сервером погоды."
}
except requests.exceptions.RequestException as e:
logger.error(f"Request exception (Forecast): {e}")
return {"success": False, "error": "Превышено время ожидания или ошибка сети."}
return {
"success": False,
"error": "Превышено время ожидания или ошибка сети."
}
+11
View File
@@ -0,0 +1,11 @@
# weather_utils.py
def get_weather_comment(temperature):
"""Возвращает текстовый комментарий и эмодзи в зависимости от температуры"""
if temperature >= 25:
return "Жара! ☀️"
elif temperature <= 0:
return "Дубак! ❄️"
else:
return "Нормальная погода ☁️"