Files
SaraFem/SaraSortdInfo.py
T

175 lines
6.5 KiB
Python

"""Read-only SaraSortd integration (Features/Major: "Shows what types of files
go to somewhere else, or where types of files come from"). Reads SaraSortd's
own GlobalConf.toml plus each relevant directory's [email protected] sidecar file
directly as TOML — never imports or runs SaraSortd itself, and never writes to
any of its files.
Only replicates the one substitution InputDir/OutputDir/ValidInputDirs entries
actually use in practice: %Name% tokens resolved against [DirectoryPaths], and
the Parent placeholder ("@" by default) swapped for a directory's own name.
SaraSortd's fuller Parse() also handles NextNum/Year/OrgFileName/etc, but those
only matter for file *renaming*, not this read-only "what goes where" view."""
import fnmatch
import re
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from Core import AppConfig
@dataclass(frozen=True)
class SortRule:
"""One line of what GetSortRules found: Pattern goes Direction ("->" for
outgoing, "<-" for incoming) Other (the other directory, display-
formatted). Structured rather than a pre-formatted string so Browser.py
can group rules sharing the same (Direction, Other) into one row —
Bugs/Major: "SaraSortd integration header is way too long... condense
outputs and inputs into > then v button things"."""
Pattern: str
Direction: str
Other: str
DefaultParentChar = "@"
DefaultDirConfName = ".@Config"
def _LoadToml(TomlPath: Path) -> Optional[dict]:
try:
return tomllib.loads(TomlPath.read_text())
except (OSError, tomllib.TOMLDecodeError):
return None
def _ResolveRootDirTokens(Value: str, DirectoryPaths: dict) -> str:
def Replace(Match: re.Match) -> str:
Name = Match.group(1)
Found = DirectoryPaths.get(Name) if Name else None
return Found if isinstance(Found, str) else Match.group(0)
return re.sub(r"%(.*?)%", Replace, Value)
def _DirConfFileName(NamePattern: str, ParentChar: str, DirName: str) -> str:
return f"{NamePattern.replace(ParentChar, DirName)}.toml"
def _ValidInputDirMatches(Pattern: str, ParentChar: str, OutputDirName: str, InputPath: Path) -> bool:
Resolved = Pattern.replace(ParentChar, OutputDirName)
return fnmatch.fnmatchcase(str(InputPath), Resolved)
def _DisplayPath(DirPath: Path) -> str:
try:
return f"~/{DirPath.relative_to(Path.home())}"
except ValueError:
return str(DirPath)
def GetSortRules(CurrentPath: Path, Config: AppConfig) -> list[SortRule]:
"""Returns SortRules such as Pattern="*.png", Direction="->",
Other="~/Pictures" (CurrentPath is a configured SaraSortd input
directory) and/or Direction="<-" ones (CurrentPath is a configured
output directory). Empty list if the integration is off, unconfigured
(including SaraSortd's own un-set-up RootDir = "Unset"), or nothing
matches this directory. Never raises — a malformed or foreign-format
SaraSortd config degrades to no info rather than breaking the browser."""
if not Config.SaraSortd.Enabled or not Config.SaraSortd.GlobalConfPath:
return []
try:
return _GetSortRulesUnsafe(CurrentPath, Config)
except Exception:
return []
def _GetSortRulesUnsafe(CurrentPath: Path, Config: AppConfig) -> list[SortRule]:
GlobalConfPath = Path(Config.SaraSortd.GlobalConfPath).expanduser()
GlobalConf = _LoadToml(GlobalConfPath)
if not GlobalConf:
return []
DirectoryPaths = GlobalConf.get("DirectoryPaths")
if not isinstance(DirectoryPaths, dict):
return []
RootDir = DirectoryPaths.get("RootDir")
if not isinstance(RootDir, str) or not RootDir or RootDir == "Unset":
return []
Names = GlobalConf.get("Names") or {}
ParentChar = (GlobalConf.get("Variables") or {}).get("Parent", DefaultParentChar)
OutDirConfName = Names.get("OutDirConfName", DefaultDirConfName)
def ResolvedDirs(Key: str) -> list[Path]:
Raw = DirectoryPaths.get(Key, [])
if not isinstance(Raw, list):
return []
return [
Path(_ResolveRootDirTokens(Entry, DirectoryPaths)).expanduser()
for Entry in Raw if isinstance(Entry, str)
]
InputDirs = ResolvedDirs("InputDir")
OutputDirs = ResolvedDirs("OutputDir")
CurrentResolved = CurrentPath.expanduser().resolve()
IsInputDir = any(D.exists() and D.resolve() == CurrentResolved for D in InputDirs)
IsOutputDir = any(D.exists() and D.resolve() == CurrentResolved for D in OutputDirs)
Results: list[SortRule] = []
if IsInputDir:
for OutputDir in OutputDirs:
if not OutputDir.is_dir():
continue
OutputDirName = OutputDir.name
OutConf = _LoadToml(OutputDir / _DirConfFileName(OutDirConfName, ParentChar, OutputDirName))
if not OutConf:
continue
ValidInputDirs = OutConf.get("ValidInputDirs", ["*"])
if not isinstance(ValidInputDirs, list):
continue
if not any(
_ValidInputDirMatches(V, ParentChar, OutputDirName, CurrentResolved)
for V in ValidInputDirs if isinstance(V, str)
):
continue
for FileRule in OutConf.get("Files") or []:
if isinstance(FileRule, dict):
Results.append(SortRule(FileRule.get("Pattern", "*"), "->", _DisplayPath(OutputDir)))
if IsOutputDir:
OutputDirName = CurrentResolved.name
OutConf = _LoadToml(CurrentResolved / _DirConfFileName(OutDirConfName, ParentChar, OutputDirName))
if OutConf:
ValidInputDirs = OutConf.get("ValidInputDirs", ["*"])
Patterns = [
FileRule.get("Pattern", "*")
for FileRule in (OutConf.get("Files") or [])
if isinstance(FileRule, dict)
]
if isinstance(ValidInputDirs, list) and Patterns:
for InputDir in InputDirs:
if not InputDir.is_dir():
continue
if not any(
_ValidInputDirMatches(V, ParentChar, OutputDirName, InputDir.resolve())
for V in ValidInputDirs if isinstance(V, str)
):
continue
for Pattern in Patterns:
Results.append(SortRule(Pattern, "<-", _DisplayPath(InputDir)))
Seen: set[SortRule] = set()
Deduped = []
for Rule in Results:
if Rule not in Seen:
Seen.add(Rule)
Deduped.append(Rule)
return Deduped