#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Общие вспомогательные функции HTML Report. В этом модуле не должно быть SQL-запросов. Только: • форматирование • HTML escaping • работа с файлами • чтение CSV • чтение gps_track.nmea • вычисления """ from __future__ import annotations import csv import html import os from datetime import datetime, timezone from pathlib import Path from zoneinfo import ZoneInfo from .config import TIMEZONE # ============================================================ # HTML # ============================================================ def esc(value) -> str: """ Безопасный вывод значения в HTML. """ if value is None: return "" return html.escape(str(value)) # ============================================================ # Значение по умолчанию # ============================================================ def safe(value, default="-"): if value is None: return default if value == "": return default return value # ============================================================ # Размер файла # ============================================================ def format_size(size): if size is None: return "-" size = float(size) units = [ "B", "KB", "MB", "GB", "TB" ] for unit in units: if size < 1024: return f"{size:.1f} {unit}" size /= 1024 return f"{size:.1f} PB" def file_size(path): path = Path(path) if not path.exists(): return "-" return format_size( path.stat().st_size ) # ============================================================ # Дата / время # ============================================================ def parse_datetime(text): if not text: return None try: dt = datetime.fromisoformat(text) # # Старые записи проекта хранят UTC # как naive datetime. # if dt.tzinfo is None: dt = dt.replace( tzinfo=timezone.utc ) return dt except Exception: return None def format_datetime( text, output_format="%d.%m.%Y %H:%M:%S" ): dt = parse_datetime(text) if dt is None: return safe(text) display_timezone = ZoneInfo(TIMEZONE) return dt.astimezone( display_timezone ).strftime( output_format ) def format_duration(start, end): start = parse_datetime(start) end = parse_datetime(end) if start is None: return "-" if end is None: return "-" delta = end - start seconds = int( delta.total_seconds() ) hours = seconds // 3600 minutes = ( seconds % 3600 ) // 60 seconds = seconds % 60 return ( f"{hours:02}:{minutes:02}:{seconds:02}" ) # Совместимость со старым кодом duration = format_duration # ============================================================ # CSV # ============================================================ def read_csv(filename): filename = Path(filename) if not filename.exists(): return [] with open( filename, newline="", encoding="utf-8", errors="ignore" ) as f: reader = csv.DictReader(f) return list(reader) # ============================================================ # Текстовый файл # ============================================================ def read_lines(filename): filename = Path(filename) if not filename.exists(): return [] with open( filename, encoding="utf-8", errors="ignore" ) as f: return [ line.rstrip() for line in f ] # ============================================================ # gps_track.nmea # ============================================================ def read_nmea(filename): """ Читает gps_track.nmea. Формат строки: 2026-07-16T12:47:51.938726;$GPRMC,... Возвращает: [ ( datetime, "$GPRMC,..." ), ... ] """ result = [] filename = Path(filename) if not filename.exists(): return result with open( filename, encoding="utf-8", errors="ignore" ) as f: for line in f: line = line.strip() if not line: continue if ";" not in line: continue timestamp, sentence = line.split( ";", 1 ) dt = parse_datetime( timestamp ) if dt is None: continue result.append( ( dt, sentence ) ) return result # ============================================================ # Каталог # ============================================================ def list_directories(path): path = Path(path) if not path.exists(): return [] return sorted( [ item for item in path.iterdir() if item.is_dir() ] ) # ============================================================ # Список файлов # ============================================================ def list_files(path): path = Path(path) if not path.exists(): return [] return sorted( [ item for item in path.iterdir() if item.is_file() ] ) # ============================================================ # Проверка существования # ============================================================ def exists(path): return Path(path).exists() # ============================================================ # Имя файла # ============================================================ def filename(path): return Path(path).name # ============================================================ # Имя каталога # ============================================================ def dirname(path): return Path(path).parent.name # ============================================================ # Последнее изменение # ============================================================ def modified(path): path = Path(path) if not path.exists(): return "-" display_timezone = ZoneInfo(TIMEZONE) return datetime.fromtimestamp( path.stat().st_mtime, timezone.utc, ).astimezone( display_timezone ).strftime( "%d.%m.%Y %H:%M:%S" ) # ============================================================ # Количество строк файла # ============================================================ def line_count(filename): filename = Path(filename) if not filename.exists(): return 0 with open( filename, encoding="utf-8", errors="ignore" ) as f: return sum( 1 for _ in f )