repair translations

This commit is contained in:
parent 98581132dd
commit 84adc5d941
18 changed files with 304 additions and 122 deletions

View file

@ -16,7 +16,7 @@ auth_service = AuthService()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate, translations: Translations = Depends(get_translations())):
async def register(user: UserCreate, translations: Translations = Depends(get_translations)):
"""Регистрирует нового пользователя.
Создаёт нового пользователя на основе предоставленных данных и возвращает
@ -44,7 +44,7 @@ async def register(user: UserCreate, translations: Translations = Depends(get_tr
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось зарегистрировать пользователя"))
@router.post("/login", response_model=Token)
async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations())):
async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations)):
"""Авторизует пользователя и создаёт сессию.
Проверяет учетные данные пользователя и возвращает токен и session_id для
@ -75,7 +75,7 @@ async def login(user: UserLogin, request: Request, translations: Translations =
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось авторизовать пользователя"))
@router.get("/me")
async def get_current_user(request: Request, translations: Translations = Depends(get_translations())):
async def get_current_user(request: Request, translations: Translations = Depends(get_translations)):
"""Возвращает данные текущего пользователя и токен.
Проверяет активную сессию по client_id и возвращает информацию о пользователе
@ -109,7 +109,7 @@ async def get_current_user(request: Request, translations: Translations = Depend
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить данные пользователя"))
@router.get("/logout")
async def logout(request: Request, translations: Translations = Depends(get_translations())):
async def logout(request: Request, translations: Translations = Depends(get_translations)):
"""Выход из системы, удаление сессии.
Удаляет активную сессию пользователя по client_id.
@ -136,7 +136,7 @@ async def logout(request: Request, translations: Translations = Depends(get_tran
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось выйти из системы"))
@router.get("/check_json")
async def check_users_file(translations: Translations = Depends(get_translations())):
async def check_users_file(translations: Translations = Depends(get_translations)):
"""Проверяет, существует ли файл users.json и содержит ли он пользователей.
Возвращает информацию о состоянии файла и наличии пользователей.

View file

@ -8,7 +8,7 @@ router = APIRouter()
ip_service = IPService()
@router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations())):
async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Проверяет доступность IP-адреса перед его применением.
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
@ -37,7 +37,7 @@ async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Transl
)
@router.post("/apply_ip")
async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations())):
async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Применяет проверенные IP-настройки к указанному сетевому профилю.
Обновляет настройки соединения через NetworkManager, отключает и
@ -74,7 +74,7 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Transl
)
@router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations())):
async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Применяет проверенные IP-настройки для отката сетевого профиля.
Используется для сохранения и отката настроек сетевого профиля к предыдущему

View file

@ -9,7 +9,7 @@ from models.network import (
NetworkInterface,
WiFiDevice,
)
from utils.i18n import get_translations, _
from utils.i18n import get_translations, _, _load_translations
from babel.support import Translations
logging.basicConfig(level=logging.INFO)
@ -44,7 +44,7 @@ class ContainerPositionManager:
},
}
def get_position_and_state(self, container_name: str, translations: Translations = None) -> Tuple[int, int, bool]:
def get_position_and_state(self, container_name: str, translations: Translations) -> Tuple[int, int, bool]:
"""Получает текущие координаты и состояние указанного контейнера.
Args:
@ -57,7 +57,6 @@ class ContainerPositionManager:
Raises:
ValueError: Если указан неверный идентификатор контейнера.
"""
translations = translations or get_translations()()
container = self.positions.get(container_name)
if not container:
raise ValueError(translations.gettext("Неверное имя контейнера: {container_name}").format(container_name=container_name))
@ -80,7 +79,7 @@ class ContainerPositionManager:
container_manager = ContainerPositionManager()
async def get_network_data(translations: Translations = None) -> Dict[str, Dict]:
async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]:
"""Возвращает текущие сетевые данные в виде словаря.
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные,
@ -97,15 +96,14 @@ async def get_network_data(translations: Translations = None) -> Dict[str, Dict]
Raises:
HTTPException: Если произошла ошибка при получении данных (status_code=500).
"""
translations = translations or get_translations()()
logger.debug("Calling get_network_data()")
try:
network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation"))
deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation"))
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state("activation", translations)
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state("deactivation", translations)
activation = Activation(
id=1,
@ -152,7 +150,7 @@ def _split_interfaces(
return activation_interfaces, deactivation_interfaces
@router.post("/update_container_position")
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = None):
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = Depends(get_translations)):
"""Обновляет позицию и состояние контейнера.
Принимает данные о новом положении и состоянии контейнера и обновляет их
@ -175,7 +173,6 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
HTTPException: Если отсутствуют обязательные поля (status_code=400) или
типы данных неверны.
"""
translations = translations or get_translations()()
try:
required_keys = [
"container_id", "container_x", "container_y", "isOpen"
@ -206,7 +203,7 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e)))
@router.get("/get_hostname")
async def get_hostname(translations: Translations = Depends(get_translations())):
async def get_hostname(translations: Translations = Depends(get_translations)):
"""Возвращает имя хоста системы.
Выполняет запрос к сервису для получения имени хоста.

View file

@ -16,7 +16,7 @@ router = APIRouter()
settings_service = SettingsService()
@router.post("/clear_subnet")
def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations())):
def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети, сохранённые в devices.json.
Удаляет все записи об узлах подсети, оставляя файл пустым.
@ -46,7 +46,7 @@ def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depe
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_wifi")
def clear_wifi(translations: Translations = Depends(get_translations())):
def clear_wifi(translations: Translations = Depends(get_translations)):
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
Удаляет все записи о точках доступа, оставляя файл пустым.
@ -71,7 +71,7 @@ def clear_wifi(translations: Translations = Depends(get_translations())):
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_all_data")
def clear_all_data(translations: Translations = Depends(get_translations())):
def clear_all_data(translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
Удаляет все записи из devices.json и wifi.json.
@ -98,7 +98,7 @@ def clear_all_data(translations: Translations = Depends(get_translations())):
WIFI_PATH = "storage/wifi/wifi.json"
@router.post("/export_devices", response_class=FileResponse)
async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations())):
async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON для указанной подсети.
@ -125,7 +125,7 @@ async def export_devices(request: SubnetScanRequest = Body(...), translations: T
)
@router.get("/export_wifi", response_class=FileResponse)
async def export_wifi(translations: Translations = Depends(get_translations())):
async def export_wifi(translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON.
@ -149,7 +149,7 @@ async def export_wifi(translations: Translations = Depends(get_translations())):
)
@router.post("/set_language")
def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations())):
def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations)):
"""
Устанавливает куку locale для смены языка пользователя.
@ -177,7 +177,7 @@ def set_language(locale: str, response: Response, translations: Translations = D
return {"message": translations.gettext("Язык успешно установлен: {locale}").format(locale=locale)}
@router.get("/get_system_language")
async def get_system_language(request: Request, translations: Translations = Depends(get_translations())):
async def get_system_language(request: Request, translations: Translations = Depends(get_translations)):
"""Выводит текущий язык, установленный в системе на основе locale.
Выполняет команду 'locale' в системе и извлекает параметры LANG

View file

@ -15,7 +15,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.post("/devices", response_model=SubnetScanResult)
def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())):
def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Сканирует локальную подсеть и возвращает данные об устройствах.
Выполняет сканирование указанной подсети для обнаружения активных устройств.
@ -44,7 +44,7 @@ def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetSe
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования подсети: {error}").format(error=str(e)))
@router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations())):
def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них.
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
@ -67,7 +67,7 @@ def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_transl
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования Wi-Fi: {error}").format(error=str(e)))
@router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())):
def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Добавляет заметку для устройства по MAC-адресу.
Привязывает заметку к устройству, идентифицируемому по MAC-адресу.
@ -101,7 +101,7 @@ def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Dep
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/devices/{mac}/note")
def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())):
def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Обновляет заметку для устройства по MAC-адресу.
Изменяет существующую заметку, привязанную к устройству.
@ -135,7 +135,7 @@ def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService =
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/devices/{mac}/note")
def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())):
def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Удаляет заметку для устройства по MAC-адресу.
Удаляет привязанную заметку для указанного устройства.
@ -168,7 +168,7 @@ def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_ser
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/{ssid}/note")
def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations())):
def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Добавляет заметку для сети Wi-Fi по SSID.
Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID.
@ -202,7 +202,7 @@ def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations =
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/wifi/{ssid}/note")
def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations())):
def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Обновляет заметку для сети Wi-Fi по SSID.
Изменяет существующую заметку, привязанную к Wi-Fi сети.
@ -236,7 +236,7 @@ def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/wifi/{ssid}/note")
def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations())):
def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)):
"""Удаляет заметку для сети Wi-Fi по SSID.
Удаляет привязанную заметку для указанной Wi-Fi сети.
@ -269,7 +269,7 @@ def delete_wifi_note(ssid: str, translations: Translations = Depends(get_transla
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/connect")
def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations())):
def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)):
"""Подключается к Wi-Fi сети по SSID и паролю.
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
@ -297,7 +297,7 @@ def connect_to_wifi(request: WifiConnectRequest, translations: Translations = De
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка подключения к Wi-Fi: {error}").format(error=str(e)))
@router.post("/wifi/disconnect")
def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations())):
def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)):
"""Отключается от Wi-Fi сети по указанному SSID.
Выполняет отключение от указанной Wi-Fi сети.
@ -324,7 +324,7 @@ def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translati
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка отключения от Wi-Fi: {error}").format(error=str(e)))
@router.get("/wifi/status")
def get_wifi_status(translations: Translations = Depends(get_translations())):
def get_wifi_status(translations: Translations = Depends(get_translations)):
"""Проверяет статус текущего подключения к Wi-Fi.
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.

View file

@ -4,7 +4,7 @@ from typing import Set
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
from starlette.websockets import WebSocketState
from utils.i18n import get_translations, _
from utils.i18n import get_translations_ws, _, _load_translations
from babel.support import Translations
from .network import update_container_position, get_network_data
@ -78,7 +78,7 @@ class WebSocketManager:
manager = WebSocketManager()
@router.websocket("/api/v1")
async def websocket_endpoint(websocket: WebSocket, translations: Translations = Depends(get_translations())):
async def websocket_endpoint(websocket: WebSocket, translations: Translations = Depends(get_translations_ws)):
"""Обработчик WebSocket-соединения.
Управляет соединением клиента, принимает данные для обновления позиций
@ -99,10 +99,10 @@ async def websocket_endpoint(websocket: WebSocket, translations: Translations =
logger.info(f"Received data: {data}")
if all(key in data for key in ["container_id", "container_x", "container_y"]):
await update_container_position(data)
await update_container_position(data, translations=translations)
# Внутренний вызов без DI, поэтому используем fallback-переводы
network_data = await get_network_data(translations=get_translations()(websocket))
# Для внутреннего вызова используем переводы из WebSocket зависимостии
network_data = await get_network_data(translations=translations)
logger.info(f"Current network data: {network_data}")
await manager.broadcast(network_data)
@ -139,7 +139,7 @@ async def broadcast_data(translations: Translations = None):
Raises:
Exception: Если произошла ошибка при получении данных или трансляции.
"""
translations = translations or get_translations()()
translations = translations or _load_translations("ru")
while True:
try:
network_data = await get_network_data(translations=translations)

130
SRV/logs/app.log Normal file
View file

@ -0,0 +1,130 @@
2025-11-17 12:33:54,542 - watchfiles.main - INFO - 4 changes detected
2025-11-17 12:33:54,553 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:54,553 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:54,553 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:54,554 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:54,554 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:54,554 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:54,894 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:55,245 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:55,595 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:55,946 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:56,297 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:56,594 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:56,594 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:56,594 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:56,594 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:56,595 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:56,595 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:56,648 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:56,999 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:57,350 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:57,701 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:58,052 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:58,403 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:58,638 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:58,638 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:58,639 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:58,639 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:58,639 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined
2025-11-17 12:33:58,639 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:33:58,754 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:59,105 - watchfiles.main - INFO - 2 changes detected
2025-11-17 12:33:59,571 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:33:59,922 - watchfiles.main - INFO - 4 changes detected
2025-11-17 12:34:00,273 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:00,624 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:00,975 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:01,326 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:01,677 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:02,028 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:02,379 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:02,730 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:03,081 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:03,432 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:03,782 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:04,133 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:04,484 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:34:04,835 - watchfiles.main - INFO - 1 change detected
2025-11-17 12:55:17,396 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:17,397 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:17,397 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:17,397 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:17,397 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:17,397 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:19,407 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:19,408 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:19,408 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:19,408 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:19,408 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:19,408 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:39,690 - api.v1.network - ERROR - Error in get_network_data: The name org.freedesktop.NetworkManager was not provided by any .service files
2025-11-17 12:55:39,690 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:39,691 - api.v1.network - ERROR - Error in get_network_data: The name org.freedesktop.NetworkManager was not provided by any .service files
2025-11-17 12:55:39,691 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:39,691 - api.v1.network - ERROR - Error in get_network_data: The name org.freedesktop.NetworkManager was not provided by any .service files
2025-11-17 12:55:39,691 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:41,721 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:41,721 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:41,721 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:41,721 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 12:55:41,721 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice
mode.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/string_type
mode.int
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.12/v/int_type
2025-11-17 12:55:41,721 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные
2025-11-17 17:45:00,050 - asyncio - ERROR - Task was destroyed but it is pending!
task: <Task pending name='Task-4' coro=<broadcast_data() done, defined at /home/merelden/main/projects/gonets/SRV/api/v1/websocket.py:130> wait_for=<Future pending cb=[Task.task_wakeup()]>>
2025-11-17 17:45:00,050 - asyncio - ERROR - Task was destroyed but it is pending!
task: <Task pending name='Task-5' coro=<broadcast_data() done, defined at /home/merelden/main/projects/gonets/SRV/api/v1/websocket.py:130> wait_for=<Future pending cb=[Task.task_wakeup()]>>

View file

@ -28,6 +28,24 @@ import uvicorn
from api.router import router
from config.cors import CORSMiddlewareConfig
from services.network_service import NetworkService
import logging
from pathlib import Path
# Логируем в файл logs/app.log и одновременно в stdout
LOG_DIR = Path(__file__).resolve().parent / "logs"
LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = LOG_DIR / "app.log"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler(LOG_FILE, encoding="utf-8"),
],
force=True, # переопределяем базовую конфигурацию, заданную в импортируемых модулях
)
# Глушим шум от hot-reload watchdog
logging.getLogger("watchfiles").setLevel(logging.WARNING)
app = FastAPI(
title="GONETS",

View file

@ -113,7 +113,7 @@ class WiFiDevice(NetworkInterface):
bitrate: Optional[int] = None
strength: Optional[int] = None
frequency: Optional[float] = None
mode: Union[str, int] = None
mode: Optional[Union[str, int]] = None
class NetworkInterfacesContainer(BaseModel):

View file

@ -20,7 +20,7 @@ import os
import logging
import netifaces
from netaddr import IPNetwork
from utils.i18n import get_translations, _
from utils.i18n import _, _load_translations
from babel.support import Translations
logger = logging.getLogger(__name__)
@ -54,7 +54,7 @@ class NetworkScanner:
Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес.
"""
self.translations = translations or get_translations()()
self.translations = translations or _load_translations("ru")
self.interface = None
self.ip_info = []
self.networks = []

View file

@ -9,7 +9,7 @@ from typing import Optional
import configparser
import os
import logging
from utils.i18n import get_translations, _
from utils.i18n import _, _load_translations
from babel.support import Translations
logging.basicConfig(level=logging.INFO)
@ -38,7 +38,7 @@ class AuthService:
def __init__(self, translations: Optional[Translations] = None):
self.storage = JsonStorage()
self.session_storage = SessionStorage()
self.translations = translations or get_translations()()
self.translations = translations or _load_translations("ru")
def create_access_token(self, data: dict) -> str:
"""Создаёт токен"""

View file

@ -18,7 +18,7 @@
import subprocess
import logging
from typing import Tuple, Optional
from utils.i18n import get_translations, _
from utils.i18n import _load_translations, _
from babel.support import Translations
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
@ -36,7 +36,7 @@ class WifiConnectionService:
Автоматически определяет доступный Wi-Fi-интерфейс и сохраняет его имя.
"""
self.interface = self._get_wifi_interface()
self.translations = translations or get_translations()()
self.translations = translations or _load_translations("ru")
def _get_wifi_interface(self) -> str:
"""Автоматически определяет Wi-Fi-интерфейс.

View file

@ -21,7 +21,7 @@ from sdbus_async.networkmanager import (
import sdbus
from sdbus_async.networkmanager.settings import ConnectionProfile
from services.arp_service import NetworkScanner
from utils.i18n import get_translations
from utils.i18n import _load_translations
from babel.support import Translations
from fastapi import Depends
@ -54,7 +54,7 @@ class IPService:
self.bus = None
self.bus = new_bus
async def check_ip_config(self, args: Dict[str, Any], translations: Translations = Depends(get_translations())) -> Dict[str, Any]:
async def check_ip_config(self, args: Dict[str, Any], translations: Translations = None) -> Dict[str, Any]:
"""Проверяет доступность IP-адреса перед его применением.
Выполняет ARP-сканирование сети для проверки, занят ли указанный IP-адрес.
@ -73,6 +73,7 @@ class IPService:
Raises:
Exception: Если произошла ошибка при сканировании ARP.
"""
translations = translations or _load_translations("ru")
self._reset_bus()
clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
@ -86,7 +87,7 @@ class IPService:
"message": translations.gettext("Настройки сетевого профиля доступны для использования.")
}
async def apply_ip_config(self, args: Dict[str, Any], translations: Translations = Depends(get_translations())) -> None:
async def apply_ip_config(self, args: Dict[str, Any], translations: Translations = None) -> None:
"""Применяет проверенные настройки IP к указанному соединению.
Перезапускает шину D-Bus, обновляет настройки соединения через NetworkManager,
@ -105,6 +106,7 @@ class IPService:
Note:
Требуются права для изменения настроек через NetworkManager.
"""
translations = translations or _load_translations("ru")
self._reset_bus()
nm = NetworkManager()
settings = NetworkManagerSettings()

View file

@ -32,9 +32,20 @@ from models.network import (
WiFiDevice,
)
import subprocess
from utils.i18n import get_translations, _
from utils.i18n import _, _load_translations
from babel.support import Translations
def _extract_value(value):
"""Приводит значения D-Bus к примитивам Python."""
if isinstance(value, tuple) and len(value) == 2:
value = value[1]
if isinstance(value, bytes):
try:
return value.decode("utf-8")
except Exception:
return value.decode("utf-8", errors="ignore")
return value
class NetworkService:
"""Сервис для взаимодействия с NetworkManager через D-Bus.
@ -51,7 +62,7 @@ class NetworkService:
self.system_bus = sd_bus_open_system()
self.nm = NetworkManager(self.system_bus)
self.nm_settings = NetworkManagerSettings(self.system_bus)
self.translations = translations or get_translations()()
self.translations = translations or _load_translations("ru")
async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str:
"""Получает метод IP (например, "auto", "manual") для указанного устройства.
@ -152,7 +163,7 @@ class NetworkService:
wifi_info = {
"ssid": None,
"bssid": None,
"bitrate": await wifi_device.bitrate,
"bitrate": _extract_value(await wifi_device.bitrate),
"strength": None,
"frequency": None,
"mode": None,
@ -160,11 +171,11 @@ class NetworkService:
if active_ap != "/":
ap = AccessPoint(active_ap, self.system_bus)
wifi_info.update({
"ssid": await ap.ssid,
"bssid": await ap.hw_address,
"strength": await ap.strength,
"frequency": await ap.frequency,
"mode": await ap.mode,
"ssid": _extract_value(await ap.ssid),
"bssid": _extract_value(await ap.hw_address),
"strength": _extract_value(await ap.strength),
"frequency": _extract_value(await ap.frequency),
"mode": _extract_value(await ap.mode),
})
return wifi_info
@ -209,8 +220,8 @@ class NetworkService:
"device_mtu": str(await generic_device.mtu),
"ipv4": IPConfig(configurations=await self.get_ip_config(generic_device, "4")),
"ipv6": IPConfig(configurations=await self.get_ip_config(generic_device, "6")),
"x": initial_positions.get(interface_name, (100, 100))[0],
"y": initial_positions.get(interface_name, (100, 100))[1],
"container_x": initial_positions.get(interface_name, (100, 100))[0],
"container_y": initial_positions.get(interface_name, (100, 100))[1],
}
if device_type == "WIFI":

View file

@ -2,7 +2,7 @@ import json
import logging
from typing import Optional
from utils.i18n import get_translations
from utils.i18n import _load_translations
from babel.support import Translations
from pathlib import Path
@ -15,7 +15,7 @@ class SettingsService:
def __init__(self, translations: Translations = None):
self.subnet_base_path = "storage/subnets"
self.wifi_file = "storage/wifi/wifi.json"
self.translations = translations or get_translations()()
self.translations = translations or _load_translations("ru")
def clear_subnet(self, subnet: Optional[str] = None) -> bool:
"""Очищает данные узлов подсети в указанном файле или во всех файлах.

View file

@ -27,7 +27,7 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
def get_subnet_service(translations: Translations = Depends(get_translations())):
def get_subnet_service(translations: Translations = Depends(get_translations)):
"""Фабричная функция для создания экземпляра SubnetService.
Использует зависимости FastAPI для получения объекта переводов и возвращает

View file

@ -9,7 +9,7 @@ import pywifi
from models.wifi import WifiAccessPoint, WifiScanResult
from pathlib import Path
import json
from utils.i18n import get_translations
from utils.i18n import _load_translations
from babel.support import Translations
# Настройка логирования
@ -36,7 +36,7 @@ class WifiStorage:
def __init__(self, storage_path: str = "storage/wifi/wifi.json", translations: Translations = None):
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
self.translations = translations or get_translations()()
self.translations = translations or _load_translations("ru")
logger.info(f"Wi-Fi storage initialized at: {self.storage_file.absolute()}")
def load_notes(self) -> Dict[str, Optional[str]]:
@ -88,10 +88,10 @@ class WifiService:
self.mac_lookup = MacLookup()
self.wifi = pywifi.PyWiFi()
self.interface = self._get_wifi_interface()
validated_translations = translations or get_translations()()
validated_translations = translations or _load_translations("ru")
if not hasattr(validated_translations, "gettext"):
logger.warning("Translations object does not support gettext, using fallback")
validated_translations = get_translations()()
validated_translations = _load_translations("ru")
self.translations = validated_translations
self.storage = WifiStorage()

View file

@ -2,65 +2,90 @@
Утилиты локализации и зависимости переводов для FastAPI.
Содержимое модуля:
- `get_translations()`: фабрика зависимости FastAPI, считывающая локаль из
query-параметра `locale`, cookie `locale` или заголовка `Accept-Language`
(по приоритету), с дефолтом `ru`. Возвращает функцию-зависимость, которая
подгружает `babel.support.Translations` из каталога `SRV/locales`.
- `_()`: удобная обёртка для получения перевода строки с использованием
текущей локали (или дефолтного `ru`, если контекст запроса не передан).
Особенности:
- При отсутствии словаря перевода для выбранной локали выполняется fallback на
русскую локаль, что гарантирует наличие строк интерфейса.
- Локаль определяется только по первому языку из `Accept-Language` (без
региональных суффиксов) для упрощения.
- Модуль ведёт отладочное логирование выбора и загрузки локали.
- `get_translations`: зависимость FastAPI, считывающая локаль из query/cookie/header.
- `get_translations_ws`: аналог для WebSocket.
- `_load_translations`: низкоуровневая загрузка словаря переводов с fallback.
- `_()`: обёртка для gettext с дефолтом ru.
"""
from fastapi import Request, WebSocket
from babel.support import Translations
import logging
from typing import Optional, Union
from pathlib import Path
import os
from typing import Iterable, Optional
logger = logging.getLogger(__name__)
LOCALES_DIR = Path(__file__).resolve().parent.parent / "locales"
def _candidate_locale_dirs() -> Iterable[Path]:
"""Возвращает возможные каталоги с переводами."""
configured = os.getenv("GONETS_LOCALES_PATH")
if configured:
configured_path = Path(configured).expanduser()
if configured_path.exists():
yield configured_path
srv_root = Path(__file__).resolve().parent.parent
for candidate in (
srv_root / "locales",
srv_root.parent / "locales", # на случай, если каталоги вынесены на уровень выше
Path("/usr/share/gonets/locales"),
Path("/usr/local/share/gonets/locales"),
):
if candidate.exists():
yield candidate
LOCALE_DIRS = list(dict.fromkeys(_candidate_locale_dirs()))
if not LOCALE_DIRS:
logger.warning("Locales directory not found; translations will use NullTranslations")
def _load_translations(locale: str) -> Translations:
"""Подгружает словарь переводов с fallback на ru."""
last_error: Optional[str] = None
for directory in LOCALE_DIRS or [None]:
try:
translations = Translations.load(str(LOCALES_DIR), [locale], "messages")
if not translations._catalog:
logger.warning(f"Translations for locale {locale} not found, falling back to ru")
translations = Translations.load(str(LOCALES_DIR), ["ru"], "messages")
if directory is None:
raise FileNotFoundError("Locales directory list is empty")
translations = Translations.load(str(directory), [locale], "messages")
if translations._catalog:
return translations
except Exception as e: # pragma: no cover - логируем и возвращаем ru
logger.error(f"Failed to load translations for locale {locale}: {str(e)}")
return Translations.load(str(LOCALES_DIR), ["ru"], "messages")
logger.warning(
"Translations catalog empty for locale %s in %s; trying fallback ru",
locale,
directory,
)
ru_translations = Translations.load(str(directory), ["ru"], "messages")
if ru_translations._catalog:
return ru_translations
except Exception as exc: # pragma: no cover - логируем и возвращаем ru
last_error = str(exc)
continue
logger.error(
"Failed to load translations for locale %s from dirs %s: %s. Using NullTranslations",
locale,
LOCALE_DIRS,
last_error,
)
return Translations()
def get_translations() -> callable:
"""
Создаёт зависимость для загрузки переводов на основе заголовка Accept-Language
или параметра запроса 'locale'. По умолчанию используется русский язык.
"""
def translations_dependency(request: Optional[Union[Request, WebSocket]] = None) -> Translations:
if request is None:
logger.warning("Translations dependency called without request/websocket; fallback to ru")
return _load_translations("ru")
# Проверяем параметр запроса 'locale', если он есть
locale = None
if hasattr(request, "query_params"):
def _resolve_locale_from_request(request: Request) -> str:
locale = request.query_params.get("locale")
if not locale and hasattr(request, "cookies"):
if not locale:
locale = request.cookies.get("locale")
if not locale and hasattr(request, "headers"):
if not locale:
locale = request.headers.get("Accept-Language", "ru").split(",")[0].split("-")[0]
return locale or "ru"
locale = locale or "ru"
logger.debug(f"Determined locale: {locale}")
def get_translations(request: Request) -> Translations:
"""Зависимость FastAPI для HTTP-эндпоинтов."""
locale = _resolve_locale_from_request(request)
logger.debug(f"Determined locale (http): {locale}")
return _load_translations(locale)
return translations_dependency
def get_translations_ws(websocket: WebSocket) -> Translations:
"""Зависимость FastAPI для WebSocket-эндпоинтов."""
# WebSocket не имеет cookies/headers в виде HTTPRequest, но есть headers
header_locale = websocket.headers.get("accept-language", "ru").split(",")[0].split("-")[0] if websocket.headers else "ru"
logger.debug(f"Determined locale (ws): {header_locale}")
return _load_translations(header_locale or "ru")
def _(msgid: str, request: Request = None) -> str:
"""
@ -74,10 +99,9 @@ def _(msgid: str, request: Request = None) -> str:
str: Переведенная строка.
"""
if request is None:
# Язык, который будет отображаться по умолчанию
translations = _load_translations("ru")
logger.debug(f"Using default ru translations for msgid: {msgid}")
else:
translations = get_translations()(request)
translations = get_translations(request)
logger.debug(f"Using translations for locale {request.cookies.get('locale', 'ru')} for msgid: {msgid}")
return translations.gettext(msgid)