mirror of
https://git.srhtnin.garden/sarah/SaraTag.git
synced 2026-10-13 18:28:16 +02:00
121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""SaraTag's data library, .saratag/Core.py, loaded the same way SaraFem and
|
|
SaraView load it (by file path, as "SaraTagCore"). That file stays
|
|
stdlib-only and keeps its location and function names, because SaraFem and
|
|
SaraView point their CoreModulePath at .saratag/.
|
|
|
|
Also the shared helpers on top of it: reading and writing TOML, Books and
|
|
SaraTagConf.toml.
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
import tomllib
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import tomlkit
|
|
|
|
_CorePath = Path(__file__).resolve().parent / ".saratag" / "Core.py"
|
|
_Spec = importlib.util.spec_from_file_location("SaraTagCore", _CorePath)
|
|
TagCore = importlib.util.module_from_spec(_Spec)
|
|
sys.modules["SaraTagCore"] = TagCore
|
|
_Spec.loader.exec_module(TagCore)
|
|
|
|
DataDir: Path = TagCore.ApplicationDirectory
|
|
ConfPath: Path = TagCore.SaraTagConfPath
|
|
RecordsPath: Path = TagCore.SaraTagRecordsPath
|
|
HistoryPath: Path = TagCore.SaraTagHistoryPath
|
|
LexiconsDir = DataDir / "Lexicons"
|
|
|
|
TagTypes = ["Text", "Number", "Rating", "List", "Boolean"]
|
|
Unset = "?" # a Book value nobody has filled in yet
|
|
|
|
|
|
class DataError(Exception):
|
|
"""A SaraTag data file that can't be read or written; the message is shown as is."""
|
|
|
|
|
|
def ReadToml(FilePath: Path) -> dict[str, Any]:
|
|
try:
|
|
with FilePath.open("rb") as TomlFile:
|
|
return tomllib.load(TomlFile)
|
|
except tomllib.TOMLDecodeError as Error:
|
|
raise DataError(f"Invalid TOML in {FilePath.name}: {Error}") from Error
|
|
except OSError as Error:
|
|
raise DataError(f"Couldn't read {FilePath}: {Error}") from Error
|
|
|
|
|
|
def WriteAtomically(FilePath: Path, Text: str) -> None:
|
|
"""Write through a temporary sibling, so a crash never leaves half a file."""
|
|
TemporaryPath = FilePath.with_name(f"{FilePath.name}.tmp")
|
|
try:
|
|
TemporaryPath.write_text(Text, encoding="utf-8")
|
|
TemporaryPath.replace(FilePath)
|
|
finally:
|
|
TemporaryPath.unlink(missing_ok=True)
|
|
|
|
|
|
def TomlValue(Value: Any) -> str:
|
|
"""One value in TOML syntax, escaped properly (newlines and all)."""
|
|
return tomlkit.item(Value).as_string()
|
|
|
|
|
|
def TagType(Definition: dict[str, Any]) -> str:
|
|
""""Text", "Number", ... as written in TagTypes, whatever the case in the file."""
|
|
Raw = str(Definition.get("Type", "")).strip().casefold()
|
|
return next((Name for Name in TagTypes if Name.casefold() == Raw), "")
|
|
|
|
|
|
def Bound(Definition: dict[str, Any], Name: str) -> Any:
|
|
"""Minimum or Maximum (also accepting the old Min/Max spelling), or None."""
|
|
Value = Definition.get(Name, Definition.get(Name[:3]))
|
|
if isinstance(Value, bool) or not isinstance(Value, (int, float)):
|
|
return None
|
|
return Value
|
|
|
|
|
|
def FormatValue(Value: Any, Definition: dict[str, Any] | None = None) -> str:
|
|
"""A Book value for display: ratings as symbols, booleans as Yes/No."""
|
|
if Definition and TagType(Definition) == "Rating" and isinstance(Value, int) and not isinstance(Value, bool):
|
|
return TagCore.FormatRating(Value, Definition)
|
|
if isinstance(Value, bool):
|
|
return "Yes" if Value else "No"
|
|
if isinstance(Value, list):
|
|
return ", ".join(str(Item) for Item in Value)
|
|
return str(Value)
|
|
|
|
|
|
# SaraTagConf.toml
|
|
|
|
def LoadConf() -> tomlkit.TOMLDocument:
|
|
TagCore.EnsureConfigFiles()
|
|
try:
|
|
return tomlkit.parse(ConfPath.read_text(encoding="utf-8"))
|
|
except (OSError, tomlkit.exceptions.TOMLKitError) as Error:
|
|
raise DataError(f"Couldn't read {ConfPath.name}: {Error}") from Error
|
|
|
|
|
|
def ConfValue(Section: str, Key: str, Fallback: Any) -> Any:
|
|
try:
|
|
Table = LoadConf().get(Section, {})
|
|
except DataError:
|
|
return Fallback
|
|
Value = Table.get(Key, Fallback) if isinstance(Table, dict) else Fallback
|
|
return Value.unwrap() if hasattr(Value, "unwrap") else Value
|
|
|
|
|
|
def SaveConfValues(Values: dict[str, dict[str, Any]]) -> None:
|
|
"""{Section: {Key: Value}} into SaraTagConf.toml, keeping its comments."""
|
|
Document = LoadConf()
|
|
for Section, Keys in Values.items():
|
|
if Section not in Document:
|
|
Document[Section] = tomlkit.table()
|
|
for Key, Value in Keys.items():
|
|
Document[Section][Key] = Value
|
|
WriteAtomically(ConfPath, tomlkit.dumps(Document))
|
|
|
|
|
|
def ExpandUserPath(Text: str) -> Path:
|
|
return Path(os.path.expanduser(Text.strip()))
|