transcript-viewer 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ """Small JSON files under ~/.transcript-viewer, written so a crash cannot truncate them.
2
+
3
+ The library and the settings both keep a single JSON document that must survive
4
+ an interrupted write — losing every annotation, or a stored credential, because
5
+ a process died mid-`write()` is not an acceptable failure. Both go through here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import tempfile
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ ROOT = Path.home() / ".transcript-viewer"
17
+
18
+ # What the tool was called before, and where it kept things then. A rename
19
+ # should not look like data loss, so a library left by the old name is adopted
20
+ # once, on the first run that finds one.
21
+ LEGACY_ROOT = Path.home() / ".atif"
22
+
23
+
24
+ def adopt_legacy() -> bool:
25
+ """Take over a library the tool left under its former name.
26
+
27
+ Returns whether anything moved. Absolute paths recorded inside the index
28
+ point into the old directory, so they are rewritten as part of the move —
29
+ a copy left behind would be worse than not moving at all.
30
+ """
31
+ if ROOT.exists() or not LEGACY_ROOT.exists():
32
+ return False
33
+
34
+ LEGACY_ROOT.rename(ROOT) # same filesystem, so this is atomic
35
+
36
+ index = ROOT / "index.json"
37
+ if index.exists():
38
+ text = index.read_text()
39
+ moved = text.replace(str(LEGACY_ROOT), str(ROOT))
40
+ if moved != text:
41
+ index.write_text(moved)
42
+ return True
43
+
44
+
45
+ def read_json(path: Path) -> dict[str, Any]:
46
+ """A JSON object, or {} if the file is missing or damaged.
47
+
48
+ Refusing to start because one byte is wrong is worse than starting empty,
49
+ so a corrupt file is treated as absent rather than raised.
50
+ """
51
+ try:
52
+ data = json.loads(path.read_text())
53
+ except (OSError, json.JSONDecodeError):
54
+ return {}
55
+ return data if isinstance(data, dict) else {}
56
+
57
+
58
+ def write_json(path: Path, payload: dict[str, Any], private: bool = False) -> None:
59
+ """Write atomically: temp file in the same directory, then os.replace.
60
+
61
+ Same directory so the replace is a rename within one filesystem, which is
62
+ atomic; a temp file elsewhere would degrade to a copy. `private` marks a
63
+ file only the owner may read — for anything holding a credential.
64
+ """
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ if private:
67
+ os.chmod(path.parent, 0o700)
68
+
69
+ handle = tempfile.NamedTemporaryFile(
70
+ "w", dir=path.parent, prefix=f".{path.stem}-", suffix=".tmp", delete=False
71
+ )
72
+ try:
73
+ with handle as out:
74
+ out.write(json.dumps(payload, indent=2))
75
+ out.flush()
76
+ os.fsync(out.fileno())
77
+ # mkstemp already creates at 0600; set it anyway so the guarantee is
78
+ # stated here rather than inherited from a library's implementation.
79
+ os.chmod(handle.name, 0o600 if private else 0o644)
80
+ os.replace(handle.name, path)
81
+ except BaseException:
82
+ Path(handle.name).unlink(missing_ok=True)
83
+ raise