Исправил форматирование кода по PEP8

This commit is contained in:
Leo
2026-07-09 12:59:10 +05:00
parent d0f08abed7
commit becb16f282
6 changed files with 161 additions and 102 deletions
+12 -9
View File
@@ -14,23 +14,23 @@ DB_NAME = os.getenv("DB_NAME", "postgres")
DB_USER = os.getenv("DB_USER", "postgres") DB_USER = os.getenv("DB_USER", "postgres")
DB_PASS = os.getenv("DB_PASS", "") DB_PASS = os.getenv("DB_PASS", "")
@contextmanager @contextmanager
def get_db_connection(): def get_db_connection():
# Используем RealDictCursor, чтобы код в tg.py мог обращаться к полям как к словарю: user['default_city'] # Используем RealDictCursor, чтобы код в tg.py мог обращаться к полям как к словарю: user['default_city']
conn = psycopg2.connect( conn = psycopg2.connect(host=DB_HOST,
host=DB_HOST,
port=DB_PORT, port=DB_PORT,
dbname=DB_NAME, dbname=DB_NAME,
user=DB_USER, user=DB_USER,
password=DB_PASS, password=DB_PASS,
cursor_factory=RealDictCursor cursor_factory=RealDictCursor)
)
try: try:
yield conn yield conn
finally: finally:
conn.commit() conn.commit()
conn.close() conn.close()
def init_db(): def init_db():
try: try:
with get_db_connection() as conn: with get_db_connection() as conn:
@@ -51,12 +51,15 @@ def init_db():
# Если база обязательна для запуска, можно сделать raise e, чтобы бот не стартовал впустую # Если база обязательна для запуска, можно сделать raise e, чтобы бот не стартовал впустую
raise e raise e
def get_user(telegram_id): def get_user(telegram_id):
with get_db_connection() as conn: with get_db_connection() as conn:
with conn.cursor() as cursor: with conn.cursor() as cursor:
cursor.execute('SELECT * FROM users WHERE telegram_id = %s', (telegram_id,)) cursor.execute('SELECT * FROM users WHERE telegram_id = %s',
(telegram_id, ))
return cursor.fetchone() return cursor.fetchone()
def add_user(telegram_id, username): def add_user(telegram_id, username):
with get_db_connection() as conn: with get_db_connection() as conn:
with conn.cursor() as cursor: with conn.cursor() as cursor:
@@ -64,18 +67,18 @@ def add_user(telegram_id, username):
# В PostgreSQL синтаксис избегания дубликатов - ON CONFLICT DO NOTHING # В PostgreSQL синтаксис избегания дубликатов - ON CONFLICT DO NOTHING
cursor.execute( cursor.execute(
'INSERT INTO users (telegram_id, username) VALUES (%s, %s) ON CONFLICT (telegram_id) DO NOTHING', 'INSERT INTO users (telegram_id, username) VALUES (%s, %s) ON CONFLICT (telegram_id) DO NOTHING',
(telegram_id, username) (telegram_id, username))
)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при добавлении пользователя: {e}") logger.error(f"Ошибка при добавлении пользователя: {e}")
def update_default_city(telegram_id, city): def update_default_city(telegram_id, city):
with get_db_connection() as conn: with get_db_connection() as conn:
with conn.cursor() as cursor: with conn.cursor() as cursor:
cursor.execute( cursor.execute(
'UPDATE users SET default_city = %s WHERE telegram_id = %s', 'UPDATE users SET default_city = %s WHERE telegram_id = %s',
(city, telegram_id) (city, telegram_id))
)
def get_users_count(): def get_users_count():
with get_db_connection() as conn: with get_db_connection() as conn:
+11 -11
View File
@@ -1,32 +1,32 @@
from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
def get_main_menu(): def get_main_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
markup.add( markup.add(KeyboardButton("Текущая погода"),
KeyboardButton("Текущая погода"),
KeyboardButton("Прогноз на 3 дня"), KeyboardButton("Прогноз на 3 дня"),
KeyboardButton("Прогноз на 5 дней"), KeyboardButton("Прогноз на 5 дней"),
KeyboardButton("Настройки") KeyboardButton("Настройки"))
)
return markup return markup
def get_settings_menu(): def get_settings_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1) markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
markup.add( markup.add(KeyboardButton("Изменить город"),
KeyboardButton("Изменить город"), KeyboardButton("🔙 Назад в меню"))
KeyboardButton("🔙 Назад в меню")
)
return markup return markup
def get_cancel_menu(): def get_cancel_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1) markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
markup.add(KeyboardButton("Отмена")) markup.add(KeyboardButton("Отмена"))
return markup return markup
def get_city_confirm_inline(city_name): def get_city_confirm_inline(city_name):
markup = InlineKeyboardMarkup() markup = InlineKeyboardMarkup()
markup.add( markup.add(
InlineKeyboardButton("Да, сохранить", callback_data=f"save_city:{city_name}"), InlineKeyboardButton("Да, сохранить",
InlineKeyboardButton("Нет, отмена", callback_data="cancel_city") callback_data=f"save_city:{city_name}"),
) InlineKeyboardButton("Нет, отмена", callback_data="cancel_city"))
return markup return markup
+3
View File
@@ -1,14 +1,17 @@
# test_bot.py # test_bot.py
from weather_utils import get_weather_comment from weather_utils import get_weather_comment
def test_weather_comment_hot(): def test_weather_comment_hot():
# Проверяем, что при +30 будет "Жара! ☀️" # Проверяем, что при +30 будет "Жара! ☀️"
assert get_weather_comment(30) == "Жара! ☀️" assert get_weather_comment(30) == "Жара! ☀️"
def test_weather_comment_cold(): def test_weather_comment_cold():
# Проверяем, что при -10 будет "Дубак! ❄️" # Проверяем, что при -10 будет "Дубак! ❄️"
assert get_weather_comment(-10) == "Дубак! ❄️" assert get_weather_comment(-10) == "Дубак! ❄️"
def test_weather_comment_normal(): def test_weather_comment_normal():
# Проверяем промежуточное значение # Проверяем промежуточное значение
assert get_weather_comment(15) == "Нормальная погода ☁️" assert get_weather_comment(15) == "Нормальная погода ☁️"
+68 -35
View File
@@ -11,8 +11,7 @@ import weather_api as wa
# Настройка логирования # Настройка логирования
logging.basicConfig( logging.basicConfig(
level=logging.INFO, 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__) logger = logging.getLogger(__name__)
# Инициализация переменных окружения # Инициализация переменных окружения
@@ -23,11 +22,13 @@ WEBHOOK_HOST = os.getenv("WEBHOOK_HOST") # e.g., https://my-bot.com
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8080)) WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8080))
if not TG_TOKEN or not WEATHER_API_KEY: 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) exit(1)
if not WEBHOOK_HOST: if not WEBHOOK_HOST:
logger.error("Не задана переменная WEBHOOK_HOST! (например: https://mydomain.com)") logger.error(
"Не задана переменная WEBHOOK_HOST! (например: https://mydomain.com)")
exit(1) exit(1)
# Формируем URL вебхука (скрываем токен в пути для безопасности) # Формируем URL вебхука (скрываем токен в пути для безопасности)
@@ -43,12 +44,14 @@ bot = telebot.TeleBot(TG_TOKEN)
# Инициализация Flask приложения # Инициализация Flask приложения
app = Flask(__name__) app = Flask(__name__)
def get_user_city(user_id): def get_user_city(user_id):
user = db.get_user(user_id) user = db.get_user(user_id)
if user and user['default_city']: if user and user['default_city']:
return user['default_city'] return user['default_city']
return None return None
@bot.message_handler(commands=['start', 'help']) @bot.message_handler(commands=['start', 'help'])
def send_welcome(message): def send_welcome(message):
user_id = message.from_user.id user_id = message.from_user.id
@@ -60,23 +63,31 @@ def send_welcome(message):
welcome_text = ( welcome_text = (
"Привет! 🌍 Я твой погодный бот.\n\n" "Привет! 🌍 Я твой погодный бот.\n\n"
"Я могу показывать текущую погоду и прогноз на несколько дней.\n" "Я могу показывать текущую погоду и прогноз на несколько дней.\n"
"Для удобства давай установим твой город по умолчанию." "Для удобства давай установим твой город по умолчанию.")
)
user_city = get_user_city(user_id) user_city = get_user_city(user_id)
if user_city: if user_city:
welcome_text += f"\n\nТвой текущий город: <b>{user_city}</b>" 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: else:
bot.send_message(message.chat.id, welcome_text, parse_mode="HTML") 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.register_next_step_handler(msg, process_city_input)
@bot.message_handler(commands=['stats']) @bot.message_handler(commands=['stats'])
def admin_stats(message): def admin_stats(message):
if ADMIN_ID and str(message.from_user.id) == str(ADMIN_ID): if ADMIN_ID and str(message.from_user.id) == str(ADMIN_ID):
count = db.get_users_count() count = db.get_users_count()
bot.send_message(message.chat.id, f"📊 Уникальных пользователей в базе: {count}") bot.send_message(message.chat.id,
f"📊 Уникальных пользователей в базе: {count}")
@bot.message_handler(func=lambda message: message.text == "Настройки") @bot.message_handler(func=lambda message: message.text == "Настройки")
def settings_menu(message): def settings_menu(message):
@@ -86,20 +97,32 @@ def settings_menu(message):
text += f"Текущий город: <b>{user_city}</b>" text += f"Текущий город: <b>{user_city}</b>"
else: else:
text += "Город по умолчанию не установлен." 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 == "🔙 Назад в меню") @bot.message_handler(func=lambda message: message.text == "🔙 Назад в меню")
def back_to_main(message): 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 == "Изменить город") @bot.message_handler(func=lambda message: message.text == "Изменить город")
def change_city_prompt(message): 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) bot.register_next_step_handler(msg, process_city_input)
def process_city_input(message): def process_city_input(message):
if message.text == "Отмена": 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 return
city = message.text.strip() city = message.text.strip()
@@ -107,56 +130,60 @@ def process_city_input(message):
if weather_data["success"]: if weather_data["success"]:
real_city_name = weather_data["city"] real_city_name = weather_data["city"]
text = ( text = (f"Город <b>{real_city_name}</b> найден!\n"
f"Город <b>{real_city_name}</b> найден!\n"
f"Текущая температура: {weather_data['temp']}°C\n\n" f"Текущая температура: {weather_data['temp']}°C\n\n"
f"Сохранить этот город как основной?" f"Сохранить этот город как основной?")
)
bot.send_message( bot.send_message(
message.chat.id, message.chat.id,
text, text,
reply_markup=kb.get_city_confirm_inline(real_city_name), reply_markup=kb.get_city_confirm_inline(real_city_name),
parse_mode="HTML" parse_mode="HTML")
)
else: else:
msg = bot.send_message( msg = bot.send_message(
message.chat.id, message.chat.id,
f"❌ {weather_data['error']}\nПопробуй ввести название еще раз или нажми 'Отмена'.", 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.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): def callback_save_city(call):
city_name = call.data.split(':', 1)[1] city_name = call.data.split(':', 1)[1]
user_id = call.from_user.id user_id = call.from_user.id
db.update_default_city(user_id, city_name) db.update_default_city(user_id, city_name)
bot.edit_message_text( bot.edit_message_text(chat_id=call.message.chat.id,
chat_id=call.message.chat.id,
message_id=call.message.message_id, message_id=call.message.message_id,
text=f"✅ Город <b>{city_name}</b> успешно сохранен!", text=f"✅ Город <b>{city_name}</b> успешно сохранен!",
parse_mode="HTML" parse_mode="HTML")
) bot.send_message(call.message.chat.id,
bot.send_message(call.message.chat.id, "Открываю главное меню:", reply_markup=kb.get_main_menu()) "Открываю главное меню:",
reply_markup=kb.get_main_menu())
bot.answer_callback_query(call.id) bot.answer_callback_query(call.id)
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_city') @bot.callback_query_handler(func=lambda call: call.data == 'cancel_city')
def callback_cancel_city(call): def callback_cancel_city(call):
bot.edit_message_text( bot.edit_message_text(chat_id=call.message.chat.id,
chat_id=call.message.chat.id,
message_id=call.message.message_id, message_id=call.message.message_id,
text="❌ Изменение города отменено." text="❌ Изменение города отменено.")
) bot.send_message(call.message.chat.id,
bot.send_message(call.message.chat.id, "Возвращаемся в главное меню:", reply_markup=kb.get_main_menu()) "Возвращаемся в главное меню:",
reply_markup=kb.get_main_menu())
bot.answer_callback_query(call.id) 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): def weather_requests(message):
user_city = get_user_city(message.from_user.id) user_city = get_user_city(message.from_user.id)
if not user_city: 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) bot.register_next_step_handler(msg, process_city_input)
return return
@@ -185,6 +212,7 @@ def weather_requests(message):
else: else:
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}") bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
@bot.message_handler(func=lambda message: True) @bot.message_handler(func=lambda message: True)
def unknown_text(message): def unknown_text(message):
city = message.text.strip() city = message.text.strip()
@@ -197,11 +225,15 @@ def unknown_text(message):
answer += f"На улице: {data['desc']}." answer += f"На улице: {data['desc']}."
bot.send_message(message.chat.id, answer, parse_mode="HTML") bot.send_message(message.chat.id, answer, parse_mode="HTML")
else: 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) --- # --- ВЕБХУКИ (Flask) ---
@app.route(WEBHOOK_PATH, methods=['POST']) @app.route(WEBHOOK_PATH, methods=['POST'])
def webhook(): def webhook():
if request.headers.get('content-type') == 'application/json': if request.headers.get('content-type') == 'application/json':
@@ -212,6 +244,7 @@ def webhook():
else: else:
abort(403) abort(403)
if __name__ == "__main__": if __name__ == "__main__":
logger.info("Удаляем старый вебхук...") logger.info("Удаляем старый вебхук...")
bot.remove_webhook() bot.remove_webhook()
+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" BASE_URL = "https://api.openweathermap.org/data/2.5"
TIMEOUT = 10 TIMEOUT = 10
def get_current_weather(city: str) -> dict: def get_current_weather(city: str) -> dict:
url = f"{BASE_URL}/weather" url = f"{BASE_URL}/weather"
params = { params = {
@@ -32,11 +33,19 @@ def get_current_weather(city: str) -> dict:
elif response.status_code == 404: elif response.status_code == 404:
return {"success": False, "error": f"Город '{city}' не найден."} return {"success": False, "error": f"Город '{city}' не найден."}
else: else:
logger.error(f"API Error: Code {response.status_code} for city {city}") logger.error(
return {"success": False, "error": "Ошибка связи с сервером погоды."} f"API Error: Code {response.status_code} for city {city}")
return {
"success": False,
"error": "Ошибка связи с сервером погоды."
}
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error(f"Request exception: {e}") logger.error(f"Request exception: {e}")
return {"success": False, "error": "Превышено время ожидания или ошибка сети."} return {
"success": False,
"error": "Превышено время ожидания или ошибка сети."
}
def get_forecast(city: str, days: int) -> dict: def get_forecast(city: str, days: int) -> dict:
url = f"{BASE_URL}/forecast" url = f"{BASE_URL}/forecast"
@@ -60,7 +69,8 @@ def get_forecast(city: str, days: int) -> dict:
# дата в формате 'YYYY-MM-DD HH:MM:SS' # дата в формате 'YYYY-MM-DD HH:MM:SS'
date_str = item["dt_txt"].split(" ")[0] date_str = item["dt_txt"].split(" ")[0]
daily_data[date_str]["temps"].append(item["main"]["temp"]) 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()) sorted_dates = sorted(daily_data.keys())
@@ -77,7 +87,8 @@ def get_forecast(city: str, days: int) -> dict:
max_temp = max(temps) 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") 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: elif response.status_code == 404:
return {"success": False, "error": f"Город '{city}' не найден."} return {"success": False, "error": f"Город '{city}' не найден."}
else: else:
logger.error(f"API Error (Forecast): Code {response.status_code} for city {city}") logger.error(
return {"success": False, "error": "Ошибка связи с сервером погоды."} f"API Error (Forecast): Code {response.status_code} for city {city}"
)
return {
"success": False,
"error": "Ошибка связи с сервером погоды."
}
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error(f"Request exception (Forecast): {e}") logger.error(f"Request exception (Forecast): {e}")
return {"success": False, "error": "Превышено время ожидания или ошибка сети."} return {
"success": False,
"error": "Превышено время ожидания или ошибка сети."
}
+1
View File
@@ -1,5 +1,6 @@
# weather_utils.py # weather_utils.py
def get_weather_comment(temperature): def get_weather_comment(temperature):
"""Возвращает текстовый комментарий и эмодзи в зависимости от температуры""" """Возвращает текстовый комментарий и эмодзи в зависимости от температуры"""
if temperature >= 25: if temperature >= 25: