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.
- transcript_viewer/__init__.py +5 -0
- transcript_viewer/ai.py +374 -0
- transcript_viewer/cli.py +87 -0
- transcript_viewer/config.py +193 -0
- transcript_viewer/corpus.py +340 -0
- transcript_viewer/fetch.py +771 -0
- transcript_viewer/library.py +169 -0
- transcript_viewer/page.html +2248 -0
- transcript_viewer/store.py +83 -0
- transcript_viewer/viewer.py +1000 -0
- transcript_viewer-0.5.0.dist-info/METADATA +440 -0
- transcript_viewer-0.5.0.dist-info/RECORD +14 -0
- transcript_viewer-0.5.0.dist-info/WHEEL +4 -0
- transcript_viewer-0.5.0.dist-info/entry_points.txt +2 -0
|
@@ -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
|