SaraTag, a Flet GUI for tagging files.

This commit is contained in:
sarah committed 2026-10-08 19:03:17 +02:00
commit 845d6c6ee6
27 files changed
+6035

No files matched your search

+988
View File
@@ -0,0 +1,988 @@
from __future__ import annotations
import copy
import sys
import tomllib
from pathlib import Path
from typing import Any
ApplicationName = "SaraTag"
ApplicationVersion = "0.1.0-beta"
def GetApplicationDirectory() -> Path:
"""Return the portable directory containing SaraTag."""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return Path(__file__).resolve().parent.parent
ApplicationDirectory = GetApplicationDirectory()
SaraTagConfPath = ApplicationDirectory / "SaraTagConf.toml"
SaraTagRecordsPath = ApplicationDirectory / ".SaraTagRecords.toml"
SaraTagHistoryPath = ApplicationDirectory / ".SaraTagHistory.toml"
DefaultSaraTagConf = f'''# SaraTag's main portable configuration file.
# Paths in this file are interpreted relative to the SaraTag directory
# unless they are absolute.
[SaraTag]
ConfigVersion = 1
ApplicationVersion = "{ApplicationVersion}"
[Files]
RecordsFile = ".SaraTagRecords.toml"
BookSuffix = ".book.toml"
[Lexicon]
AutoInheritance = true
RelativeSourcePaths = true
LexiconFilePattern = "{{Source}}_{{DirectoryName}}.toml"
[Explorer]
HideUntaggedDirectories = true
HideTagFiles = true
ShowInvalidEntries = true
ShowImagePreviews = true
[Media]
VideoPlayer = "mpv"
[History]
Enabled = true
MaxFileHistory = 30
MaxDirectoryHistory = 20
MaxUpdateHistory = 20
RememberFilter = true
RememberSorting = true
RememberLastDirectory = true
RememberLastSelectedFile = true
RememberRecentFiles = true
RememberUpdaterMode = true
RememberUpdates = true
'''
DefaultSaraTagRecords = '''# Records registered with SaraTag.
# Add records through SaraTag rather than editing this file when possible.
ConfigVersion = 1
Records = []
'''
DefaultSaraTagHistory = """# SaraTag session history.
# This file is managed automatically.
ConfigVersion = 1
"""
LexiconFilePattern = "{Source}_{DirectoryName}.toml"
BookSuffix = ".book.toml"
def GetLexiconConfig() -> dict[str, Any]:
try:
Data = LoadSaraTagConf()
except (OSError, ValueError):
return {}
Value = Data.get("Lexicon", {})
return Value if isinstance(Value, dict) else {}
def GetLexiconFilePattern() -> str:
Config = GetLexiconConfig()
Value = Config.get("LexiconFilePattern")
if isinstance(Value, str) and Value.strip():
return Value.strip()
try:
Data = LoadSaraTagConf()
except (OSError, ValueError):
return LexiconFilePattern
Files = Data.get("Files", {})
if isinstance(Files, dict):
Legacy = Files.get("LexiconFilePattern")
if isinstance(Legacy, str) and Legacy.strip():
return Legacy.strip()
return LexiconFilePattern
def GetLexiconSourceName(
DirectoryPath: Path,
SourcePath: Path | None = None,
) -> str:
if SourcePath is not None:
Stem = SourcePath.stem
if Stem.startswith("._Lexicon."):
Stem = Stem[len("._Lexicon."):]
return Stem or DirectoryPath.parent.name or DirectoryPath.name
return DirectoryPath.parent.name or DirectoryPath.name
def FormatLexiconFileName(
DirectoryPath: Path,
SourcePath: Path | None = None,
) -> str:
Values = {
"DirectoryName": DirectoryPath.name,
"Source": GetLexiconSourceName(DirectoryPath, SourcePath),
}
try:
Name = GetLexiconFilePattern().format(**Values)
except (KeyError, ValueError):
Name = LexiconFilePattern.format(**Values)
return Path(Name).name or f"{DirectoryPath.name}.toml"
def GetPreferredLexiconPath(
DirectoryPath: Path,
SourcePath: Path | None = None,
) -> Path:
return DirectoryPath / FormatLexiconFileName(
DirectoryPath,
SourcePath,
)
def GetGenericLegacyLexiconPath(
DirectoryPath: Path,
) -> Path:
"""Return the short-lived generic Lexicon path."""
return DirectoryPath / "._Lexicon.toml"
def GetLegacyLexiconPath(DirectoryPath: Path) -> Path:
"""Return the original directory-tag path."""
return DirectoryPath / f".{DirectoryPath.name}.tag.toml"
def GetLexiconPath(DirectoryPath: Path) -> Path:
"""Return an existing Lexicon path, preferring current naming."""
Candidates = (
GetPreferredLexiconPath(DirectoryPath),
GetGenericLegacyLexiconPath(DirectoryPath),
GetLegacyLexiconPath(DirectoryPath),
)
for Candidate in Candidates:
if Candidate.is_file():
return Candidate
return Candidates[0]
def MigrateLexiconPath(DirectoryPath: Path) -> Path:
"""Rename a legacy Lexicon to its canonical filename."""
PreferredPath = GetPreferredLexiconPath(DirectoryPath)
if PreferredPath.is_file():
return PreferredPath
for LegacyPath in (
GetGenericLegacyLexiconPath(DirectoryPath),
GetLegacyLexiconPath(DirectoryPath),
):
if not LegacyPath.is_file():
continue
LegacyPath.replace(PreferredPath)
return PreferredPath
return PreferredPath
def FindParentLexiconPath(DirectoryPath: Path) -> Path | None:
Current = DirectoryPath.expanduser().resolve().parent
while True:
Candidate = GetLexiconPath(Current)
if Candidate.is_file():
return Candidate
Parent = Current.parent
if Parent == Current:
return None
Current = Parent
def ResolveLexiconSourcePath(
LexiconPath: Path,
SourceValue: str,
) -> Path:
SourcePath = Path(SourceValue).expanduser()
if not SourcePath.is_absolute():
SourcePath = LexiconPath.parent / SourcePath
return SourcePath.resolve()
def MergeLexiconValues(
SourceValue: Any,
LocalValue: Any,
) -> Any:
"""Merge inherited and local values for a SubLexicon override."""
if isinstance(SourceValue, dict) and isinstance(LocalValue, dict):
Result = copy.deepcopy(SourceValue)
for Key, Value in LocalValue.items():
if Key in Result:
Result[Key] = MergeLexiconValues(
Result[Key],
Value,
)
else:
Result[Key] = copy.deepcopy(Value)
return Result
return copy.deepcopy(LocalValue)
def MergeTagDefinition(
SourceDefinition: dict[str, Any],
LocalDefinition: dict[str, Any],
) -> dict[str, Any]:
"""Apply local properties over an inherited tag definition."""
return MergeLexiconValues(
SourceDefinition,
LocalDefinition,
)
def ResolveLexiconData(
LexiconPath: Path,
SeenPaths: set[Path] | None = None,
) -> dict[str, Any]:
"""Return a Lexicon with its inherited tags resolved."""
ResolvedPath = LexiconPath.expanduser().resolve()
if SeenPaths is None:
SeenPaths = set()
if ResolvedPath in SeenPaths:
raise ValueError(
f"Circular Lexicon inheritance detected at {ResolvedPath}."
)
if not ResolvedPath.is_file():
raise OSError(f"Lexicon not found: {ResolvedPath}")
NextSeen = set(SeenPaths)
NextSeen.add(ResolvedPath)
Data = LoadToml(ResolvedPath)
Options = Data.get("Lexicon", {})
if not isinstance(Options, dict):
raise ValueError(
f"{ResolvedPath.name} has no valid Lexicon table."
)
LocalTags = Data.get("Tags", {})
if LocalTags is None:
LocalTags = {}
if not isinstance(LocalTags, dict):
raise ValueError(
f"{ResolvedPath.name} has no valid Tags table."
)
Inherit = Options.get("Inherit", False)
if not isinstance(Inherit, bool):
raise ValueError(
f"{ResolvedPath.name}: Lexicon.Inherit must be Boolean."
)
RawExcludeTags = Options.get("ExcludeTags", [])
if RawExcludeTags is None:
RawExcludeTags = []
if not isinstance(RawExcludeTags, list):
raise ValueError(
f"{ResolvedPath.name}: Lexicon.ExcludeTags must be a list."
)
ExcludeTags: list[str] = []
SeenExcludedTags: set[str] = set()
for Item in RawExcludeTags:
if not isinstance(Item, str) or not Item.strip():
raise ValueError(
f"{ResolvedPath.name}: every ExcludeTags item "
"must be a non-empty string."
)
TagName = Item.strip()
if TagName in SeenExcludedTags:
continue
SeenExcludedTags.add(TagName)
ExcludeTags.append(TagName)
EffectiveTags: dict[str, Any] = {}
if Inherit:
SourceValue = Options.get("Source")
if not isinstance(SourceValue, str) or not SourceValue.strip():
raise ValueError(
f"{ResolvedPath.name}: Inherit is true but Source is missing."
)
SourcePath = ResolveLexiconSourcePath(
ResolvedPath,
SourceValue.strip(),
)
SourceData = ResolveLexiconData(SourcePath, NextSeen)
SourceTags = SourceData.get("Tags", {})
EffectiveTags = {
TagName: copy.deepcopy(Definition)
for TagName, Definition in SourceTags.items()
if isinstance(TagName, str)
and isinstance(Definition, dict)
}
for TagName in ExcludeTags:
EffectiveTags.pop(TagName, None)
for TagName, Definition in LocalTags.items():
if not isinstance(TagName, str):
continue
if not isinstance(Definition, dict):
raise ValueError(
f"{ResolvedPath.name}: Tags.{TagName} must be a table."
)
if TagName in EffectiveTags:
EffectiveTags[TagName] = MergeTagDefinition(
EffectiveTags[TagName],
Definition,
)
else:
EffectiveTags[TagName] = copy.deepcopy(Definition)
Result = dict(Data)
Result["Tags"] = EffectiveTags
return Result
def ResolveLexiconTags(
LexiconPath: Path,
) -> dict[str, dict[str, Any]]:
Data = ResolveLexiconData(LexiconPath)
Tags = Data.get("Tags", {})
return {
TagName: copy.deepcopy(Definition)
for TagName, Definition in Tags.items()
if isinstance(TagName, str)
and isinstance(Definition, dict)
}
def ResolveRecordPath(
Record: dict[str, Any],
ApplicationBase: Path | None = None,
) -> Path:
"""Resolve a Record's collection directory from Base and Path."""
if ApplicationBase is None:
ApplicationBase = ApplicationDirectory
RawBase = Record.get("Base", str(ApplicationBase))
RawPath = Record.get("Path")
if not isinstance(RawBase, str) or not RawBase.strip():
RawBase = str(ApplicationBase)
if not isinstance(RawPath, str) or not RawPath.strip():
raise ValueError("Record has no valid Path.")
BasePath = Path(RawBase).expanduser()
if not BasePath.is_absolute():
BasePath = ApplicationBase / BasePath
PathValue = Path(RawPath).expanduser()
if PathValue.is_absolute():
return PathValue.resolve()
return (BasePath / PathValue).resolve()
def ResolveRecordLexiconPath(
Record: dict[str, Any],
ApplicationBase: Path | None = None,
) -> Path:
"""Resolve a Record's Lexicon, falling back to the collection."""
RecordPath = ResolveRecordPath(Record, ApplicationBase)
RawLexicon = Record.get("Lexicon")
if not isinstance(RawLexicon, str) or not RawLexicon.strip():
return GetLexiconPath(RecordPath)
RawBase = Record.get("Base", str(ApplicationBase or ApplicationDirectory))
BasePath = Path(str(RawBase)).expanduser()
if not BasePath.is_absolute():
BasePath = (ApplicationBase or ApplicationDirectory) / BasePath
LexiconPath = Path(RawLexicon).expanduser()
if LexiconPath.is_absolute():
return LexiconPath.resolve()
return (BasePath / LexiconPath).resolve()
def ParseEnabledRecords(
Data: dict[str, Any],
ApplicationBase: Path | None = None,
) -> tuple[list[tuple[dict[str, Any], Path, Path]], list[str]]:
"""Resolve enabled Record tables."""
RawRecords = Data.get("Record")
if RawRecords is None:
RawRecords = Data.get("Records", [])
Errors: list[str] = []
Results: list[tuple[dict[str, Any], Path, Path]] = []
if not isinstance(RawRecords, list):
return [], [".SaraTagRecords.toml has no valid Record list."]
for Index, Record in enumerate(RawRecords):
if not isinstance(Record, dict):
Errors.append(f"Record entry {Index + 1} is not a table.")
continue
if not Record.get("Enabled", True):
continue
try:
DirectoryPath = ResolveRecordPath(Record, ApplicationBase)
LexiconPath = ResolveRecordLexiconPath(Record, ApplicationBase)
except ValueError as Error:
Errors.append(f"Record entry {Index + 1}: {Error}")
continue
Results.append((Record, DirectoryPath, LexiconPath))
return Results, Errors
def FindRecordForDirectory(
DirectoryPath: Path,
RecordsData: dict[str, Any] | None = None,
ApplicationBase: Path | None = None,
) -> tuple[dict[str, Any], Path, Path] | None:
if RecordsData is None:
try:
RecordsData = LoadSaraTagRecords()
except (OSError, ValueError):
return None
Parsed, _ = ParseEnabledRecords(RecordsData, ApplicationBase)
Target = DirectoryPath.expanduser().resolve()
for Record, RecordPath, LexiconPath in Parsed:
if RecordPath == Target:
return Record, RecordPath, LexiconPath
return None
def ResolveLexiconForDirectory(
DirectoryPath: Path,
RecordsData: dict[str, Any] | None = None,
ApplicationBase: Path | None = None,
) -> Path:
Record = FindRecordForDirectory(
DirectoryPath,
RecordsData,
ApplicationBase,
)
return Record[2] if Record is not None else GetLexiconPath(DirectoryPath)
def ResolveBookLexiconPath(
BookDirectory: Path,
ParentValue: str,
RecordsData: dict[str, Any] | None = None,
ApplicationBase: Path | None = None,
) -> Path:
ParentPath = Path(ParentValue).expanduser()
if ParentPath.suffix == ".toml":
Candidate = (
ParentPath
if ParentPath.is_absolute()
else BookDirectory / ParentPath
).resolve()
if Candidate.is_file():
return Candidate
ParentDirectory = BookDirectory.resolve()
else:
ParentDirectory = (
ParentPath
if ParentPath.is_absolute()
else BookDirectory / ParentPath
).resolve()
return ResolveLexiconForDirectory(
ParentDirectory,
RecordsData,
ApplicationBase,
)
def GetBookParentValue(DirectoryPath: Path) -> str:
return str(DirectoryPath.expanduser().resolve())
def GetPreferredBookPath(FilePath: Path) -> Path:
return FilePath.parent / f".{FilePath.name}{BookSuffix}"
def GetLegacyBookPath(FilePath: Path) -> Path:
return FilePath.parent / f".{FilePath.name}.tag.toml"
def GetBookPath(FilePath: Path) -> Path:
Preferred = GetPreferredBookPath(FilePath)
if Preferred.is_file():
return Preferred
Legacy = GetLegacyBookPath(FilePath)
if Legacy.is_file():
return Legacy
return Preferred
def IsBookPath(PathValue: Path) -> bool:
if not PathValue.is_file() or not PathValue.name.startswith('.'):
return False
if (
PathValue.name.startswith("._Lexicon.")
and PathValue.name.endswith(".toml")
) or PathValue.name == "._Lexicon.toml":
return False
return PathValue.name.endswith(BookSuffix) or PathValue.name.endswith('.tag.toml')
def NormaliseTagDefinition(Definition: dict[str, Any]) -> dict[str, Any]:
Result = dict(Definition)
if 'Required' not in Result and 'Force' in Result:
Result['Required'] = Result.pop('Force')
else:
Result.pop('Force', None)
Result.setdefault('Required', False)
Result.setdefault('ForceDefault', False)
return Result
def CreateDirectoryTagFiles() -> None:
for Record, RootPath, RecordLexicon in ParseEnabledRecords(LoadSaraTagRecords())[0]:
if not RootPath.is_dir():
print(f"Missing root: {RootPath}")
continue
for Directory in RootPath.rglob("*"):
if not Directory.is_dir():
continue
TagFile = GetPreferredLexiconPath(Directory)
if TagFile.exists():
continue
TagFile.write_text(
f"""# SaraTag Lexicon
[Lexicon]
ConfigVersion = 1
Inherit = false
Name = "{Directory.name}"
""",
encoding="utf-8",
)
print(f"Created {TagFile}")
# Also create one for the Record itself.
RootTag = RecordLexicon
if not RootTag.exists():
RootTag.write_text(
f"""# SaraTag Lexicon
[Lexicon]
ConfigVersion = 1
Inherit = false
Name = "{RootPath.name}"
""",
encoding="utf-8",
)
print(f"Created {RootTag}")
def FormatTomlValue(Value: Any) -> str:
"""Convert a simple Python value into TOML syntax."""
if isinstance(Value, bool):
return "true" if Value else "false"
if isinstance(Value, str):
EscapedValue = Value.replace("\\", "\\\\").replace('"', '\\"')
return f'"{EscapedValue}"'
if isinstance(Value, (int, float)):
return str(Value)
if isinstance(Value, list):
FormattedValues = ", ".join(
FormatTomlValue(Item) for Item in Value
)
return f"[{FormattedValues}]"
raise TypeError(
f"Unsupported TOML value type: {type(Value).__name__}"
)
def FormatRating(
Value: Any,
Definition: dict[str, Any],
) -> str:
"""Render a Rating value while keeping its stored form numeric."""
if isinstance(Value, bool) or not isinstance(Value, int):
return str(Value)
FilledSymbol = Definition.get("FilledSymbol", "★")
EmptySymbol = Definition.get("EmptySymbol", "")
if not isinstance(FilledSymbol, str) or not FilledSymbol:
FilledSymbol = "★"
if not isinstance(EmptySymbol, str):
EmptySymbol = ""
FilledCount = max(0, Value)
Rendered = FilledSymbol * FilledCount
Maximum = Definition.get(
"Maximum",
Definition.get("Max"),
)
# Without a declared maximum, rendering empty symbols would falsely
# imply that the Rating has a fixed ceiling.
if (
isinstance(Maximum, int)
and not isinstance(Maximum, bool)
and Maximum >= FilledCount
and EmptySymbol
):
Rendered += EmptySymbol * (Maximum - FilledCount)
return Rendered
def GetInitialTagValue(TagDefinition: dict[str, Any]) -> Any:
Definition = NormaliseTagDefinition(TagDefinition)
if 'Default' in Definition:
return Definition['Default']
TagType = str(Definition.get('Type', '')).strip().casefold()
Required = bool(Definition.get('Required', False))
if TagType == 'boolean':
return False
if TagType == 'number':
return Definition.get('Minimum', '?') if Required else '?'
if TagType == 'rating':
if Required:
Minimum = Definition.get('Minimum', 0)
if isinstance(Minimum, int) and not isinstance(Minimum, bool) and Minimum >= 0:
return Minimum
return 0
return '?'
if TagType == 'list':
Values = Definition.get('Values', [])
if Required and isinstance(Values, list) and Values:
return Values[0]
return '?'
return '?'
def CreateFileTagFile(
FilePath: Path,
DirectoryTagPath: Path,
DirectorySchema: dict[str, Any],
) -> bool:
"""Create a metadata file for one ordinary file."""
FileTagPath = GetPreferredBookPath(FilePath)
if FileTagPath.exists():
return False
Tags = DirectorySchema.get("Tags", {})
Lines = [
"# SaraTag Book",
"",
f"Parent = {FormatTomlValue(GetBookParentValue(FilePath.parent))}",
"",
]
for TagName, TagDefinition in Tags.items():
InitialValue = GetInitialTagValue(TagDefinition)
Lines.append(
f"{TagName} = {FormatTomlValue(InitialValue)}"
)
FileTagPath.write_text(
"\n".join(Lines) + "\n",
encoding="utf-8",
)
print(f"Created file tag: {FileTagPath}")
return True
def CreateFileTagFilesForDirectory(
DirectoryPath: Path,
LexiconPathOverride: Path | None = None,
) -> int:
"""Create Books for all non-hidden files in one directory."""
DirectoryTagPath = (
LexiconPathOverride
if LexiconPathOverride is not None
else ResolveLexiconForDirectory(DirectoryPath)
)
if not DirectoryTagPath.is_file():
print(f"No directory tag config: {DirectoryTagPath}")
return 0
try:
DirectorySchema = ResolveLexiconData(DirectoryTagPath)
except (OSError, ValueError) as Error:
print(
f"Could not load directory schema "
f"{DirectoryTagPath}: {Error}",
file=sys.stderr,
)
return 0
CreatedCount = 0
for FilePath in DirectoryPath.iterdir():
if not FilePath.is_file():
continue
# Ignore all dotfiles, including SaraTag metadata files.
if FilePath.name.startswith("."):
continue
if CreateFileTagFile(
FilePath,
DirectoryTagPath,
DirectorySchema,
):
CreatedCount += 1
return CreatedCount
def CreateFileTagFiles(SaraTagRecords: dict[str, Any]) -> int:
"""Create file metadata throughout all enabled roots."""
CreatedCount = 0
for Record, RootPath, RecordLexicon in ParseEnabledRecords(SaraTagRecords)[0]:
if not RootPath.is_dir():
print(f"Missing root: {RootPath}")
continue
Directories = [RootPath]
Directories.extend(
Path(DirectoryPath)
for DirectoryPath, DirectoryNames, FileNames
in RootPath.walk()
if Path(DirectoryPath) != RootPath
)
for DirectoryPath in Directories:
CreatedCount += CreateFileTagFilesForDirectory(
DirectoryPath,
LexiconPathOverride=(
RecordLexicon
if DirectoryPath.resolve() == RootPath.resolve()
else None
),
)
return CreatedCount
def EnsureTextFile(FilePath: Path, DefaultContent: str) -> bool:
"""Create a configuration file when missing.
Returns True when a new file was created.
"""
if FilePath.exists():
return False
FilePath.write_text(DefaultContent, encoding="utf-8")
return True
def EnsureConfigFiles() -> list[Path]:
"""Ensure SaraTag's portable configuration files exist."""
CreatedFiles: list[Path] = []
if EnsureTextFile(SaraTagConfPath, DefaultSaraTagConf):
CreatedFiles.append(SaraTagConfPath)
if EnsureTextFile(SaraTagRecordsPath, DefaultSaraTagRecords):
CreatedFiles.append(SaraTagRecordsPath)
if EnsureTextFile(SaraTagHistoryPath, DefaultSaraTagHistory):
CreatedFiles.append(SaraTagHistoryPath)
return CreatedFiles
def LoadToml(FilePath: Path) -> dict[str, Any]:
"""Load a TOML file and provide a useful error on invalid syntax."""
try:
with FilePath.open("rb") as TomlFile:
return tomllib.load(TomlFile)
except tomllib.TOMLDecodeError as Error:
raise ValueError(f"Invalid TOML in {FilePath.name}: {Error}") from Error
except OSError as Error:
raise OSError(f"Could not read {FilePath}: {Error}") from Error
def SetExplorerShowImagePreviews(Enabled: bool) -> None:
"""Persist Explorer.ShowImagePreviews in SaraTagConf.toml."""
try:
Lines = SaraTagConfPath.read_text(
encoding="utf-8",
).splitlines()
except OSError as Error:
raise OSError(
f"Could not read {SaraTagConfPath}: {Error}"
) from Error
InExplorerSection = False
SettingFound = False
ExplorerSectionFound = False
UpdatedLines: list[str] = []
for Line in Lines:
Stripped = Line.strip()
if Stripped.startswith("[") and Stripped.endswith("]"):
if InExplorerSection and not SettingFound:
UpdatedLines.append(
"ShowImagePreviews = "
+ ("true" if Enabled else "false")
)
SettingFound = True
InExplorerSection = Stripped == "[Explorer]"
ExplorerSectionFound = (
ExplorerSectionFound or InExplorerSection
)
UpdatedLines.append(Line)
continue
if (
InExplorerSection
and Stripped.startswith("ShowImagePreviews")
and "=" in Stripped
):
UpdatedLines.append(
"ShowImagePreviews = "
+ ("true" if Enabled else "false")
)
SettingFound = True
continue
UpdatedLines.append(Line)
if InExplorerSection and not SettingFound:
UpdatedLines.append(
"ShowImagePreviews = "
+ ("true" if Enabled else "false")
)
SettingFound = True
if not ExplorerSectionFound:
if UpdatedLines and UpdatedLines[-1] != "":
UpdatedLines.append("")
UpdatedLines.extend(
[
"[Explorer]",
(
"ShowImagePreviews = "
+ ("true" if Enabled else "false")
),
]
)
UpdatedText = "\n".join(UpdatedLines).rstrip() + "\n"
TemporaryPath = SaraTagConfPath.with_name(
f"{SaraTagConfPath.name}.tmp"
)
try:
TemporaryPath.write_text(
UpdatedText,
encoding="utf-8",
)
TemporaryPath.replace(SaraTagConfPath)
except OSError as Error:
raise OSError(
f"Could not save {SaraTagConfPath}: {Error}"
) from Error
finally:
try:
if TemporaryPath.exists():
TemporaryPath.unlink()
except OSError:
pass
def LoadSaraTagConf() -> dict[str, Any]:
return LoadToml(SaraTagConfPath)
def LoadSaraTagRecords() -> dict[str, Any]:
return LoadToml(SaraTagRecordsPath)
def Main() -> int:
try:
CreatedFiles = EnsureConfigFiles()
SaraTagConf = LoadSaraTagConf()
SaraTagRecords = LoadSaraTagRecords()
CreateDirectoryTagFiles()
CreatedFileTagCount = CreateFileTagFiles(SaraTagRecords)
print(f"Created file tags: {CreatedFileTagCount}")
except (OSError, ValueError) as Error:
print(f"{ApplicationName}: {Error}", file=sys.stderr)
return 1
print(f"{ApplicationName} {ApplicationVersion}")
print(f"Application directory: {ApplicationDirectory}")
for CreatedFile in CreatedFiles:
print(f"Created: {CreatedFile.name}")
RootCount = len(SaraTagRecords.get("Record", SaraTagRecords.get("Records", [])))
print(f"Registered records: {RootCount}")
ConfigVersion = SaraTagConf.get("SaraTag", {}).get("ConfigVersion", "unknown")
print(f"Config version: {ConfigVersion}")
return 0
if __name__ == "__main__":
raise SystemExit(Main())
# Compatibility aliases for older plugins/modules.
LoadSaraTagRoots = LoadSaraTagRecords
SaraTagRootsPath = SaraTagRecordsPath
+1
View File
@@ -0,0 +1 @@
"""Internal SaraTag implementation modules."""