71 lines
1.8 KiB
Python
71 lines
1.8 KiB
Python
from __future__ import annotations
|
|||
|
|
|
||
|
|
import datetime
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
def stamp() -> str:
|
||
|
|
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
|
||
|
|
|
||
|
|
|
||
|
|
def write_atomic(path: Path, data: bytes) -> None:
|
||
|
|
with tempfile.NamedTemporaryFile(dir=path.parent, prefix=f".{path.name}-", delete=False) as handle:
|
||
|
|
handle.write(data)
|
||
|
|
handle.flush()
|
||
|
|
os.fsync(handle.fileno())
|
||
|
|
os.replace(handle.name, path)
|
||
|
|
|
||
|
|
|
||
|
|
def read_json(path: Path) -> object:
|
||
|
|
try:
|
||
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
|
except (OSError, ValueError, RecursionError):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def parse_line(line: str) -> dict | None:
|
||
|
|
try:
|
||
|
|
row = json.loads(line)
|
||
|
|
except (ValueError, RecursionError):
|
||
|
|
return None
|
||
|
|
return row if isinstance(row, dict) else None
|
||
|
|
|
||
|
|
|
||
|
|
def read_jsonl(path: Path) -> list[dict]:
|
||
|
|
try:
|
||
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||
|
|
except (OSError, ValueError):
|
||
|
|
return []
|
||
|
|
return [row for row in map(parse_line, lines) if row is not None]
|
||
|
|
|
||
|
|
|
||
|
|
def epoch(value: object) -> float | None:
|
||
|
|
if isinstance(value, bool) or not isinstance(value, int | float) or not 0 <= value < 1e11:
|
||
|
|
return None
|
||
|
|
return float(value)
|
||
|
|
|
||
|
|
|
||
|
|
def iso_epoch(value: object) -> float | None:
|
||
|
|
if not isinstance(value, str):
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
parsed = datetime.datetime.fromisoformat(value)
|
||
|
|
except ValueError:
|
||
|
|
return None
|
||
|
|
if parsed.tzinfo is None:
|
||
|
|
parsed = parsed.replace(tzinfo=datetime.UTC)
|
||
|
|
return parsed.timestamp()
|
||
|
|
|
||
|
|
|
||
|
|
def shown(value: object) -> str:
|
||
|
|
if value is None or value == "":
|
||
|
|
return "-"
|
||
|
|
return str(value)
|
||
|
|
|
||
|
|
|
||
|
|
def digest(text: str) -> str:
|
||
|
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|