riplex 0.1.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.
riplex/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """riplex: automates MakeMKV disc ripping and Plex-compatible file organization."""
2
+
3
+ __version__ = "0.1.0"
riplex/cache.py ADDED
@@ -0,0 +1,111 @@
1
+ """File-based JSON cache for external API responses.
2
+
3
+ Each cached item is stored as a JSON file containing the payload and a
4
+ ``fetched_at`` ISO timestamp. Items older than the configured TTL are
5
+ treated as missing.
6
+
7
+ Cache location follows OS conventions via platformdirs.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ import shutil
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ from platformdirs import user_cache_dir
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ _APP_NAME = "riplex"
24
+
25
+ # Module-level flag: when True, all reads return None (misses).
26
+ _disabled = False
27
+
28
+
29
+ def get_cache_dir() -> Path:
30
+ """Return the root cache directory, creating it if needed."""
31
+ p = Path(user_cache_dir(_APP_NAME))
32
+ p.mkdir(parents=True, exist_ok=True)
33
+ return p
34
+
35
+
36
+ def disable() -> None:
37
+ """Disable the cache globally (``--no-cache``)."""
38
+ global _disabled
39
+ _disabled = True
40
+ log.debug("Cache disabled")
41
+
42
+
43
+ def is_disabled() -> bool:
44
+ """Return whether caching is currently disabled."""
45
+ return _disabled
46
+
47
+
48
+ def _key_path(namespace: str, key: str) -> Path:
49
+ """Build the filesystem path for a cache entry."""
50
+ return get_cache_dir() / namespace / f"{key}.json"
51
+
52
+
53
+ def hash_key(value: str) -> str:
54
+ """Produce a filesystem-safe hash for arbitrary string keys."""
55
+ return hashlib.sha256(value.encode()).hexdigest()[:16]
56
+
57
+
58
+ def cache_get(namespace: str, key: str, ttl_days: int = 30) -> dict | list | None:
59
+ """Read a cached value, returning ``None`` on miss or expiry."""
60
+ if _disabled:
61
+ return None
62
+ path = _key_path(namespace, key)
63
+ if not path.exists():
64
+ return None
65
+ try:
66
+ raw = json.loads(path.read_text(encoding="utf-8"))
67
+ fetched = datetime.fromisoformat(raw["fetched_at"])
68
+ age_days = (datetime.now(timezone.utc) - fetched).total_seconds() / 86400
69
+ if age_days > ttl_days:
70
+ log.debug("Cache expired: %s/%s (%.1f days old)", namespace, key, age_days)
71
+ path.unlink(missing_ok=True)
72
+ return None
73
+ log.debug("Cache hit: %s/%s (%.1f days old)", namespace, key, age_days)
74
+ return raw["data"]
75
+ except (json.JSONDecodeError, KeyError, ValueError):
76
+ log.debug("Cache corrupt, removing: %s/%s", namespace, key)
77
+ path.unlink(missing_ok=True)
78
+ return None
79
+
80
+
81
+ def cache_set(namespace: str, key: str, data: dict | list) -> None:
82
+ """Write a value to the cache."""
83
+ if _disabled:
84
+ return
85
+ path = _key_path(namespace, key)
86
+ path.parent.mkdir(parents=True, exist_ok=True)
87
+ payload = {
88
+ "fetched_at": datetime.now(timezone.utc).isoformat(),
89
+ "data": data,
90
+ }
91
+ path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
92
+ log.debug("Cache write: %s/%s", namespace, key)
93
+
94
+
95
+ def clear(namespace: str | None = None) -> int:
96
+ """Remove cached files. Returns the number of files removed."""
97
+ base = get_cache_dir()
98
+ target = base / namespace if namespace else base
99
+ if not target.exists():
100
+ return 0
101
+ count = sum(1 for _ in target.rglob("*.json"))
102
+ if namespace:
103
+ shutil.rmtree(target, ignore_errors=True)
104
+ else:
105
+ for child in base.iterdir():
106
+ if child.is_dir():
107
+ shutil.rmtree(child, ignore_errors=True)
108
+ elif child.suffix == ".json":
109
+ child.unlink(missing_ok=True)
110
+ log.debug("Cache cleared: %s (%d files)", target, count)
111
+ return count