440 lines
7.4 KiB
Python
440 lines
7.4 KiB
Python
#!/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
|
|
) |