feat: refine assistant logic and update docs
This commit is contained in:
@@ -3,11 +3,120 @@ Weather feature module.
|
||||
Fetches weather data from Open-Meteo API.
|
||||
"""
|
||||
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from ..core.config import WEATHER_LAT, WEATHER_LON, WEATHER_CITY
|
||||
|
||||
_HTTP = requests.Session()
|
||||
_CITY_PREFIX_RE = re.compile(
|
||||
r"^(?:в|во)\s+(?:город(?:е|у)?\s+)?",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
_CITY_SPACING_RE = re.compile(r"\s+")
|
||||
_KNOWN_CITY_VARIATIONS = {
|
||||
"нью йорк": "Нью-Йорк",
|
||||
"нью-йорк": "Нью-Йорк",
|
||||
"нью йорке": "Нью-Йорк",
|
||||
"нью-йорке": "Нью-Йорк",
|
||||
"нью йорка": "Нью-Йорк",
|
||||
"нью-йорка": "Нью-Йорк",
|
||||
"нью йорком": "Нью-Йорк",
|
||||
"нью-йорком": "Нью-Йорк",
|
||||
"санкт петербург": "Санкт-Петербург",
|
||||
"санкт-петербург": "Санкт-Петербург",
|
||||
"санкт петербурге": "Санкт-Петербург",
|
||||
"санкт-петербурге": "Санкт-Петербург",
|
||||
"санкт петербурга": "Санкт-Петербург",
|
||||
"санкт-петербурга": "Санкт-Петербург",
|
||||
"санкт петербургом": "Санкт-Петербург",
|
||||
"санкт-петербургом": "Санкт-Петербург",
|
||||
"нижний новгород": "Нижний Новгород",
|
||||
"нижнем новгороде": "Нижний Новгород",
|
||||
"нижнего новгорода": "Нижний Новгород",
|
||||
"ростов на дону": "Ростов-на-Дону",
|
||||
"ростове на дону": "Ростов-на-Дону",
|
||||
"ростова на дону": "Ростов-на-Дону",
|
||||
"лос анджелес": "Лос-Анджелес",
|
||||
"лос-анджелес": "Лос-Анджелес",
|
||||
"лос анджелесе": "Лос-Анджелес",
|
||||
"лос-анджелесе": "Лос-Анджелес",
|
||||
"сан франциско": "Сан-Франциско",
|
||||
"сан-франциско": "Сан-Франциско",
|
||||
"улан удэ": "Улан-Удэ",
|
||||
"улан-удэ": "Улан-Удэ",
|
||||
}
|
||||
_SINGLE_WORD_CITY_VARIATIONS = {
|
||||
"москве": "Москва",
|
||||
"москвы": "Москва",
|
||||
"москвой": "Москва",
|
||||
"москву": "Москва",
|
||||
"лондоне": "Лондон",
|
||||
"лондона": "Лондон",
|
||||
"лондоном": "Лондон",
|
||||
"париже": "Париж",
|
||||
"парижа": "Париж",
|
||||
"парижем": "Париж",
|
||||
"берлине": "Берлин",
|
||||
"берлина": "Берлин",
|
||||
"берлином": "Берлин",
|
||||
"пекине": "Пекин",
|
||||
"пекина": "Пекин",
|
||||
"пекином": "Пекин",
|
||||
"роме": "Рим",
|
||||
"рима": "Рим",
|
||||
"римом": "Рим",
|
||||
"мадриде": "Мадрид",
|
||||
"мадрида": "Мадрид",
|
||||
"мадридом": "Мадрид",
|
||||
"сиднее": "Сидней",
|
||||
"сиднея": "Сидней",
|
||||
"сиднеем": "Сидней",
|
||||
"вашингтоне": "Вашингтон",
|
||||
"вашингтона": "Вашингтон",
|
||||
"вашингтоном": "Вашингтон",
|
||||
"сиэтле": "Сиэтл",
|
||||
"сиэтла": "Сиэтл",
|
||||
"сиэтлом": "Сиэтл",
|
||||
"бостоне": "Бостон",
|
||||
"бостона": "Бостон",
|
||||
"бостоном": "Бостон",
|
||||
"денвере": "Денвер",
|
||||
"денвера": "Денвер",
|
||||
"денвером": "Денвер",
|
||||
"хьюстоне": "Хьюстон",
|
||||
"хьюстона": "Хьюстон",
|
||||
"хьюстоном": "Хьюстон",
|
||||
"фениксе": "Феникс",
|
||||
"феникса": "Феникс",
|
||||
"фениксом": "Феникс",
|
||||
"атланте": "Атланта",
|
||||
"атланты": "Атланта",
|
||||
"атлантой": "Атланта",
|
||||
"портленде": "Портленд",
|
||||
"портленда": "Портленд",
|
||||
"портлендом": "Портленд",
|
||||
"остине": "Остин",
|
||||
"остина": "Остин",
|
||||
"остином": "Остин",
|
||||
"нэшвилле": "Нэшвилл",
|
||||
"нэшвилла": "Нэшвилл",
|
||||
"нэшвиллом": "Нэшвилл",
|
||||
"токио": "Токио",
|
||||
"торонто": "Торонто",
|
||||
"чикаго": "Чикаго",
|
||||
"майами": "Майами",
|
||||
}
|
||||
|
||||
|
||||
def _smart_title_city(text: str) -> str:
|
||||
parts = []
|
||||
for word in text.split():
|
||||
hyphen_parts = [part.capitalize() for part in word.split("-") if part]
|
||||
parts.append("-".join(hyphen_parts))
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def get_wmo_description(code: int) -> str:
|
||||
"""Decodes WMO weather code to Russian description."""
|
||||
codes = {
|
||||
@@ -72,143 +181,45 @@ def normalize_city_name(city_name: str) -> str:
|
||||
Converts city names from various grammatical cases to the base form for geocoding.
|
||||
Handles common Russian grammatical cases (падежи) for city names.
|
||||
"""
|
||||
# Convert to lowercase for comparison
|
||||
lower_city = city_name.lower()
|
||||
|
||||
# Remove common Russian location descriptors that might be included by mistake
|
||||
# For example, if someone says "в городе Волгоград", the city_name might be "городе волгоград"
|
||||
# So we want to extract just "волгоград"
|
||||
if 'городе' in lower_city:
|
||||
# Extract the part after "городе"
|
||||
parts = lower_city.split('городе')
|
||||
if len(parts) > 1:
|
||||
lower_city = parts[1].strip()
|
||||
elif 'город' in lower_city:
|
||||
# Extract the part after "город"
|
||||
parts = lower_city.split('город')
|
||||
if len(parts) > 1:
|
||||
lower_city = parts[1].strip()
|
||||
|
||||
# Common endings for different cases in Russian
|
||||
# Prepositional case endings (-е, -и, -у, etc.)
|
||||
prepositional_endings = ['е', 'и', 'у', 'о', 'й']
|
||||
genitive_endings = ['а', 'я', 'ов', 'ев', 'ин', 'ын']
|
||||
instrumental_endings = ['ом', 'ем', 'ой', 'ей']
|
||||
|
||||
# If the city ends with a prepositional ending, try removing it to get the base form
|
||||
if lower_city.endswith(tuple(prepositional_endings)):
|
||||
# Try to remove the ending and see if we get a valid base form
|
||||
base_form = lower_city
|
||||
# Try removing 1-2 characters to get the base form
|
||||
for i in range(2, 0, -1): # Try removing 2 chars, then 1 char
|
||||
if len(base_form) > i:
|
||||
potential_base = base_form[:-i]
|
||||
# Check if the removed part is a common ending
|
||||
if base_form[-i:] in ['ке', 'ме', 'не', 'ве', 'ге', 'де', 'те']:
|
||||
base_form = potential_base
|
||||
break
|
||||
elif base_form[-1] in prepositional_endings:
|
||||
base_form = base_form[:-1]
|
||||
break
|
||||
|
||||
# Special handling for common patterns
|
||||
if base_form.endswith('йорке'): # "нью-йорке" -> "нью-йорк"
|
||||
base_form = base_form[:-1] + 'к'
|
||||
elif base_form.endswith('ске'): # "москве" -> "москва", "париже" -> "париж"
|
||||
# This is more complex, but for "москве" -> "москва", "париже" -> "париж"
|
||||
# We'll handle the most common cases
|
||||
if base_form == 'москве':
|
||||
base_form = 'москва'
|
||||
elif base_form == 'париже':
|
||||
base_form = 'париж'
|
||||
elif base_form == 'лондоне':
|
||||
base_form = 'лондон'
|
||||
elif base_form == 'берлине':
|
||||
base_form = 'берлин'
|
||||
elif base_form == 'токио': # токио stays токио
|
||||
base_form = 'токио'
|
||||
else:
|
||||
# General rule: replace -е with -а or -ь
|
||||
if base_form.endswith('ске'):
|
||||
base_form = base_form[:-1] + 'а'
|
||||
elif base_form.endswith('ие'):
|
||||
base_form = base_form[:-2] + 'ия'
|
||||
|
||||
# Capitalize appropriately
|
||||
if base_form != lower_city:
|
||||
return base_form.capitalize()
|
||||
|
||||
# Dictionary mapping specific known variations
|
||||
case_variations = {
|
||||
"нью-йорке": "Нью-Йорк",
|
||||
"нью-йорка": "Нью-Йорк",
|
||||
"нью-йорком": "Нью-Йорк",
|
||||
"москве": "Москва",
|
||||
"москвы": "Москва",
|
||||
"москвой": "Москва",
|
||||
"москву": "Москва",
|
||||
"лондоне": "Лондон",
|
||||
"лондона": "Лондон",
|
||||
"лондоном": "Лондон",
|
||||
"париже": "Париж",
|
||||
"парижа": "Париж",
|
||||
"парижем": "Париж",
|
||||
"берлине": "Берлин",
|
||||
"берлина": "Берлин",
|
||||
"берлином": "Берлин",
|
||||
"пекине": "Пекин",
|
||||
"пекина": "Пекин",
|
||||
"пекином": "Пекин",
|
||||
"роме": "Рим",
|
||||
"рима": "Рим",
|
||||
"римом": "Рим",
|
||||
"мадриде": "Мадрид",
|
||||
"мадрида": "Мадрид",
|
||||
"мадридом": "Мадрид",
|
||||
"сиднее": "Сидней",
|
||||
"сиднея": "Сидней",
|
||||
"сиднеем": "Сидней",
|
||||
"вашингтоне": "Вашингтон",
|
||||
"вашингтона": "Вашингтон",
|
||||
"вашингтоном": "Вашингтон",
|
||||
"лос-анджелесе": "Лос-Анджелес",
|
||||
"лос-анджелеса": "Лос-Анджелес",
|
||||
"лос-анджелесом": "Лос-Анджелес",
|
||||
"сиэтле": "Сиэтл",
|
||||
"сиэтла": "Сиэтл",
|
||||
"сиэтлом": "Сиэтл",
|
||||
"бостоне": "Бостон",
|
||||
"бостона": "Бостон",
|
||||
"бостоном": "Бостон",
|
||||
"денвере": "Денвер",
|
||||
"денвера": "Денвер",
|
||||
"денвером": "Денвер",
|
||||
"хьюстоне": "Хьюстон",
|
||||
"хьюстона": "Хьюстон",
|
||||
"хьюстоном": "Хьюстон",
|
||||
"фениксе": "Феникс",
|
||||
"феникса": "Феникс",
|
||||
"фениксом": "Феникс",
|
||||
"атланте": "Атланта",
|
||||
"атланты": "Атланта",
|
||||
"атлантой": "Атланта",
|
||||
"портленде": "Портленд",
|
||||
"портленда": "Портленд",
|
||||
"портлендом": "Портленд",
|
||||
"остине": "Остин",
|
||||
"остина": "Остин",
|
||||
"остином": "Остин",
|
||||
"нэшвилле": "Нэшвилл",
|
||||
"нэшвилла": "Нэшвилл",
|
||||
"нэшвиллом": "Нэшвилл",
|
||||
"сан-франциско": "Сан-Франциско",
|
||||
"токио": "Токио",
|
||||
"торонто": "Торонто",
|
||||
"чикаго": "Чикаго",
|
||||
"майами": "Майами",
|
||||
}
|
||||
|
||||
return case_variations.get(lower_city, city_name)
|
||||
lowered = str(city_name or "").lower().replace("ё", "е").strip()
|
||||
if not lowered:
|
||||
return city_name
|
||||
|
||||
lowered = _CITY_PREFIX_RE.sub("", lowered)
|
||||
lowered = _CITY_SPACING_RE.sub(" ", lowered).strip(" -")
|
||||
if not lowered:
|
||||
return city_name
|
||||
|
||||
exact_match = _KNOWN_CITY_VARIATIONS.get(lowered)
|
||||
if exact_match:
|
||||
return exact_match
|
||||
|
||||
single_word_match = _SINGLE_WORD_CITY_VARIATIONS.get(lowered)
|
||||
if single_word_match:
|
||||
return single_word_match
|
||||
|
||||
spaced = lowered.replace("-", " ")
|
||||
exact_match = _KNOWN_CITY_VARIATIONS.get(spaced)
|
||||
if exact_match:
|
||||
return exact_match
|
||||
|
||||
if " " not in spaced:
|
||||
for suffix, replacement in (
|
||||
("ом", ""),
|
||||
("ем", ""),
|
||||
("ой", "а"),
|
||||
("ей", "а"),
|
||||
("е", ""),
|
||||
("у", "а"),
|
||||
("ю", "я"),
|
||||
):
|
||||
if spaced.endswith(suffix) and len(spaced) > len(suffix) + 2:
|
||||
candidate = spaced[: -len(suffix)] + replacement
|
||||
mapped = _SINGLE_WORD_CITY_VARIATIONS.get(candidate)
|
||||
if mapped:
|
||||
return mapped
|
||||
|
||||
return _smart_title_city(lowered)
|
||||
|
||||
def get_coordinates_by_city(city_name: str) -> tuple:
|
||||
"""
|
||||
@@ -220,8 +231,9 @@ def get_coordinates_by_city(city_name: str) -> tuple:
|
||||
|
||||
# Add normalized version
|
||||
normalized_city = normalize_city_name(city_name)
|
||||
if normalized_city != city_name:
|
||||
if normalized_city and normalized_city not in try_names:
|
||||
try_names.append(normalized_city)
|
||||
normalized_lower = str(normalized_city or city_name).lower().replace("ё", "е").strip()
|
||||
|
||||
# Also try with English version if it's a known translation
|
||||
city_to_eng = {
|
||||
@@ -334,8 +346,18 @@ def get_coordinates_by_city(city_name: str) -> tuple:
|
||||
}
|
||||
|
||||
eng_name = city_to_eng.get(city_name.lower())
|
||||
if eng_name:
|
||||
normalized_eng_name = city_to_eng.get(normalized_lower)
|
||||
if eng_name and eng_name not in try_names:
|
||||
try_names.append(eng_name)
|
||||
if normalized_eng_name and normalized_eng_name not in try_names:
|
||||
try_names.append(normalized_eng_name)
|
||||
|
||||
if normalized_city:
|
||||
hyphen_variant = normalized_city.replace(" ", "-")
|
||||
space_variant = normalized_city.replace("-", " ")
|
||||
for variant in (hyphen_variant, space_variant):
|
||||
if variant and variant not in try_names:
|
||||
try_names.append(variant)
|
||||
|
||||
# Try each name in sequence
|
||||
for name_to_try in try_names:
|
||||
|
||||
Reference in New Issue
Block a user