Refactor logging, clean dead code, fix temp files and async tasks
- Replace f-string logging with % formatting throughout - Fix create_temp_file: use mkstemp() instead of mkdtemp() (no leaked dirs) - Add cleanup background thread and copy.deepcopy to AsyncTaskManager - Fix transcribe_audio_async signature to accept transcription_service + params - Use resample_poly instead of scipy_resample for better audio quality - Remove unused validate_file/BinaryIO methods from FileValidator - Add requests to requirements.txt - Add error handling for pip install in server.sh - Add __all__ and module docstrings to __init__.py files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1e778cae86
commit
ca2e41e0f5
@@ -0,0 +1 @@
|
||||
"""Модуль infrastructure — вспомогательные компоненты сервиса."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Модуль async_tasks.py содержит функции для асинхронной обработки задач.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import uuid
|
||||
import time
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
@@ -27,6 +28,7 @@ class AsyncTaskManager:
|
||||
"""
|
||||
self._lock = Lock()
|
||||
self.tasks: Dict[str, Dict[str, Any]] = {}
|
||||
self._start_cleanup_thread()
|
||||
|
||||
def run_task(self, func: Callable, *args, **kwargs) -> str:
|
||||
"""
|
||||
@@ -91,14 +93,14 @@ class AsyncTaskManager:
|
||||
self.tasks[task_id]["result"] = result
|
||||
self.tasks[task_id]["completed_at"] = time.time()
|
||||
|
||||
logger.info(f"Задача {task_id} завершена успешно")
|
||||
logger.info("Задача %s завершена успешно", task_id)
|
||||
except Exception as e:
|
||||
with self._lock:
|
||||
self.tasks[task_id]["status"] = "failed"
|
||||
self.tasks[task_id]["error"] = str(e)
|
||||
self.tasks[task_id]["completed_at"] = time.time()
|
||||
|
||||
logger.error(f"Задача {task_id} завершилась с ошибкой: {e}")
|
||||
logger.error("Задача %s завершилась с ошибкой: %s", task_id, e)
|
||||
|
||||
def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -112,22 +114,37 @@ class AsyncTaskManager:
|
||||
"""
|
||||
with self._lock:
|
||||
task = self.tasks.get(task_id)
|
||||
return dict(task) if task else None
|
||||
return copy.deepcopy(task) if task else None
|
||||
|
||||
def _start_cleanup_thread(self) -> None:
|
||||
"""Запуск фонового потока для периодической очистки старых задач."""
|
||||
def _cleanup_loop():
|
||||
while True:
|
||||
time.sleep(600) # Каждые 10 минут
|
||||
with self._lock:
|
||||
self._cleanup_old_tasks()
|
||||
|
||||
thread = Thread(target=_cleanup_loop, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
# Глобальный экземпляр менеджера асинхронных задач
|
||||
task_manager = AsyncTaskManager()
|
||||
|
||||
|
||||
def transcribe_audio_async(file_path: str, transcriber) -> str:
|
||||
def transcribe_audio_async(file_path: str, transcription_service, params: Dict = None) -> str:
|
||||
"""
|
||||
Асинхронная транскрибация аудиофайла.
|
||||
|
||||
Args:
|
||||
file_path: Путь к аудиофайлу.
|
||||
transcriber: Экземпляр транскрайбера.
|
||||
transcription_service: Экземпляр TranscriptionService.
|
||||
params: Параметры транскрибации.
|
||||
|
||||
Returns:
|
||||
ID задачи.
|
||||
"""
|
||||
return task_manager.run_task(transcriber.process_file, file_path)
|
||||
params = params or {}
|
||||
def _transcribe():
|
||||
return transcription_service.transcribe(file_path, "async_task", params)
|
||||
return task_manager.run_task(_transcribe)
|
||||
@@ -91,7 +91,8 @@ class RequestLogger:
|
||||
return
|
||||
g.start_time = time.time()
|
||||
self.logger.info(
|
||||
f"{request.method} {request.path} от {self._get_client_ip()}",
|
||||
"%s %s от %s",
|
||||
request.method, request.path, self._get_client_ip(),
|
||||
extra={"type": "request"}
|
||||
)
|
||||
|
||||
@@ -100,7 +101,8 @@ class RequestLogger:
|
||||
return response
|
||||
processing_time = time.time() - getattr(g, 'start_time', time.time())
|
||||
self.logger.info(
|
||||
f"{response.status_code} за {processing_time:.3f} сек",
|
||||
"%s за %.3f сек",
|
||||
response.status_code, processing_time,
|
||||
extra={"type": "response"}
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import tempfile
|
||||
import logging
|
||||
|
||||
@@ -20,15 +19,15 @@ def create_temp_file(suffix: str = ".wav") -> str:
|
||||
Returns:
|
||||
Путь к временному файлу.
|
||||
"""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_path = os.path.join(temp_dir, f"{uuid.uuid4()}{suffix}")
|
||||
logger.debug(f"Создан временный файл: {temp_path}")
|
||||
fd, temp_path = tempfile.mkstemp(suffix=suffix)
|
||||
os.close(fd)
|
||||
logger.debug("Создан временный файл: %s", temp_path)
|
||||
return temp_path
|
||||
|
||||
|
||||
def cleanup_temp_files(file_paths: list) -> None:
|
||||
"""
|
||||
Удаляет временные файлы и их директории.
|
||||
Удаляет временные файлы.
|
||||
|
||||
Args:
|
||||
file_paths: Список путей к файлам для удаления.
|
||||
@@ -37,10 +36,6 @@ def cleanup_temp_files(file_paths: list) -> None:
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
logger.debug(f"Удалён временный файл: {path}")
|
||||
|
||||
temp_dir = os.path.dirname(path)
|
||||
if os.path.exists(temp_dir) and not os.listdir(temp_dir):
|
||||
os.rmdir(temp_dir)
|
||||
logger.debug("Удалён временный файл: %s", path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось очистить временный файл {path}: {e}")
|
||||
logger.warning("Не удалось очистить временный файл %s: %s", path, e)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import os
|
||||
import magic
|
||||
from typing import Dict, List, BinaryIO
|
||||
from typing import Dict, List
|
||||
import logging
|
||||
|
||||
# Получаем логгер из централизованной настройки
|
||||
@@ -38,64 +38,6 @@ class FileValidator:
|
||||
["audio/wav", "audio/mpeg", "audio/ogg",
|
||||
"audio/flac", "audio/mp4"])
|
||||
|
||||
def validate_file(self, file: BinaryIO, filename: str) -> bool:
|
||||
"""
|
||||
Валидирует файл на основе конфигурации.
|
||||
|
||||
Args:
|
||||
file: Файловый объект.
|
||||
filename: Имя файла.
|
||||
|
||||
Returns:
|
||||
True, если файл прошел валидацию.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если файл не прошел валидацию.
|
||||
"""
|
||||
try:
|
||||
# Проверка размера файла
|
||||
self._validate_file_size(file)
|
||||
|
||||
# Проверка расширения файла
|
||||
self._validate_file_extension(filename)
|
||||
|
||||
# Проверка MIME-типа файла
|
||||
self._validate_file_mime_type(file)
|
||||
|
||||
return True
|
||||
except ValidationError as e:
|
||||
# Логирование общей ошибки валидации
|
||||
logger.warning(f"Ошибка валидации файла '{filename}': {str(e)}")
|
||||
raise
|
||||
|
||||
def _validate_file_size(self, file: BinaryIO) -> None:
|
||||
"""
|
||||
Валидирует размер файла.
|
||||
|
||||
Args:
|
||||
file: Файловый объект.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если размер файла превышает максимально допустимый.
|
||||
"""
|
||||
# Сохранение текущей позиции
|
||||
current_position = file.tell()
|
||||
|
||||
# Переход в конец файла для определения размера
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_length = file.tell()
|
||||
|
||||
# Возврат к исходной позиции
|
||||
file.seek(current_position)
|
||||
|
||||
max_size_bytes = self.max_file_size_mb * 1024 * 1024
|
||||
if file_length > max_size_bytes:
|
||||
logger.warning(f"Попытка загрузки файла размером {file_length / (1024*1024):.2f} МБ, "
|
||||
f"что превышает максимально допустимый размер {self.max_file_size_mb} МБ")
|
||||
|
||||
raise ValidationError(f"Размер файла ({file_length / (1024*1024):.2f} МБ) "
|
||||
f"превышает максимально допустимый ({self.max_file_size_mb} МБ)")
|
||||
|
||||
def _validate_file_extension(self, filename: str) -> None:
|
||||
"""
|
||||
Валидирует расширение файла.
|
||||
@@ -109,46 +51,12 @@ class FileValidator:
|
||||
if not any(filename.lower().endswith(ext.lower()) for ext in self.allowed_extensions):
|
||||
# Логирование попытки загрузки файла с неразрешенным расширением
|
||||
file_extension = os.path.splitext(filename)[1]
|
||||
logger.warning(f"Попытка загрузки файла с неразрешенным расширением '{file_extension}'. "
|
||||
f"Имя файла: {filename}. Разрешенные расширения: {', '.join(self.allowed_extensions)}")
|
||||
logger.warning("Попытка загрузки файла с неразрешенным расширением '%s'. "
|
||||
"Имя файла: %s. Разрешенные расширения: %s", file_extension, filename, ", ".join(self.allowed_extensions))
|
||||
|
||||
raise ValidationError(f"Расширение файла не разрешено. "
|
||||
f"Разрешенные расширения: {', '.join(self.allowed_extensions)}")
|
||||
|
||||
def _validate_file_mime_type(self, file: BinaryIO) -> None:
|
||||
"""
|
||||
Валидирует MIME-тип файла.
|
||||
|
||||
Args:
|
||||
file: Файловый объект.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если MIME-тип файла не входит в список разрешенных.
|
||||
"""
|
||||
# Сохранение текущей позиции
|
||||
current_position = file.tell()
|
||||
|
||||
try:
|
||||
# Чтение первых байтов для определения MIME-типа
|
||||
header = file.read(1024)
|
||||
mime_type = magic.from_buffer(header, mime=True)
|
||||
|
||||
# Возврат к исходной позиции
|
||||
file.seek(current_position)
|
||||
|
||||
if mime_type not in self.allowed_mime_types:
|
||||
# Логирование попытки загрузки файла с неразрешенным MIME-типом
|
||||
logger.warning(f"Попытка загрузки файла с неразрешенным MIME-типом '{mime_type}'. "
|
||||
f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}")
|
||||
|
||||
raise ValidationError(f"MIME-тип файла ({mime_type}) не разрешен. "
|
||||
f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}")
|
||||
except Exception as e:
|
||||
# Возврат к исходной позиции в случае ошибки
|
||||
file.seek(current_position)
|
||||
logger.warning(f"Не удалось определить MIME-тип файла: {e}")
|
||||
# Не прерываем валидацию, если не удалось определить MIME-тип
|
||||
|
||||
def validate_file_by_path(self, file_path: str, filename: str) -> bool:
|
||||
"""
|
||||
Валидирует файл по пути на диске.
|
||||
@@ -182,7 +90,6 @@ class FileValidator:
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось определить MIME-тип файла: {e}")
|
||||
logger.warning("Не удалось определить MIME-тип файла: %s", e)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user