confluence-markdown 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.
@@ -0,0 +1,3 @@
1
+ """Confluence Data Center Markdown Tool."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,173 @@
1
+ """Simple file-based cache for API responses."""
2
+
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Default cache directory
13
+ DEFAULT_CACHE_DIR = Path.home() / ".cache" / "confluence-markdown"
14
+
15
+ # Default TTL in seconds (1 hour)
16
+ DEFAULT_TTL = 3600
17
+
18
+ # Default max number of cache entries before oldest are evicted
19
+ DEFAULT_MAX_ENTRIES = 500
20
+
21
+
22
+ class Cache:
23
+ """Simple file-based cache with TTL support."""
24
+
25
+ def __init__(
26
+ self,
27
+ cache_dir: Optional[Path] = None,
28
+ ttl: int = DEFAULT_TTL,
29
+ enabled: bool = True,
30
+ max_entries: int = DEFAULT_MAX_ENTRIES,
31
+ ):
32
+ self.cache_dir = cache_dir or DEFAULT_CACHE_DIR
33
+ self.ttl = ttl
34
+ self.enabled = enabled
35
+ self.max_entries = max_entries
36
+
37
+ if self.enabled:
38
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ def _get_cache_key(self, key: str) -> str:
41
+ """Generate a safe filename from cache key."""
42
+ return hashlib.md5(key.encode()).hexdigest()
43
+
44
+ def _get_cache_path(self, key: str) -> Path:
45
+ """Get full path for a cache key."""
46
+ return self.cache_dir / f"{self._get_cache_key(key)}.json"
47
+
48
+ def get(self, key: str) -> Optional[Any]:
49
+ """
50
+ Get value from cache.
51
+
52
+ Args:
53
+ key: Cache key
54
+
55
+ Returns:
56
+ Cached value or None if not found/expired
57
+ """
58
+ if not self.enabled:
59
+ return None
60
+
61
+ cache_path = self._get_cache_path(key)
62
+ if not cache_path.exists():
63
+ return None
64
+
65
+ try:
66
+ with open(cache_path) as f:
67
+ data = json.load(f)
68
+
69
+ # Check TTL
70
+ if time.time() - data.get("timestamp", 0) > self.ttl:
71
+ logger.debug("Cache expired for key: %s", key[:50])
72
+ cache_path.unlink(missing_ok=True)
73
+ return None
74
+
75
+ logger.debug("Cache hit for key: %s", key[:50])
76
+ return data.get("value")
77
+
78
+ except (json.JSONDecodeError, OSError) as e:
79
+ logger.debug("Cache read error: %s", e)
80
+ return None
81
+
82
+ def set(self, key: str, value: Any) -> None:
83
+ """
84
+ Store value in cache.
85
+
86
+ Args:
87
+ key: Cache key
88
+ value: Value to cache (must be JSON-serializable)
89
+ """
90
+ if not self.enabled:
91
+ return
92
+
93
+ cache_path = self._get_cache_path(key)
94
+ try:
95
+ data = {
96
+ "timestamp": time.time(),
97
+ "key": key[:100],
98
+ "value": value,
99
+ }
100
+ with open(cache_path, "w") as f:
101
+ json.dump(data, f)
102
+ logger.debug("Cached value for key: %s", key[:50])
103
+ self._evict_if_needed()
104
+ except (TypeError, OSError) as e:
105
+ logger.debug("Cache write error: %s", e)
106
+
107
+ def delete(self, key: str) -> None:
108
+ """Delete a cached value."""
109
+ if not self.enabled:
110
+ return
111
+
112
+ cache_path = self._get_cache_path(key)
113
+ cache_path.unlink(missing_ok=True)
114
+
115
+ def clear(self) -> int:
116
+ """
117
+ Clear all cached values.
118
+
119
+ Returns:
120
+ Number of cache files deleted
121
+ """
122
+ if not self.enabled or not self.cache_dir.exists():
123
+ return 0
124
+
125
+ count = 0
126
+ for cache_file in self.cache_dir.glob("*.json"):
127
+ try:
128
+ cache_file.unlink()
129
+ count += 1
130
+ except OSError:
131
+ pass
132
+
133
+ logger.info("Cleared %d cache files", count)
134
+ return count
135
+
136
+ def _evict_if_needed(self) -> None:
137
+ """Remove oldest entries when cache exceeds max_entries."""
138
+ cache_files = list(self.cache_dir.glob("*.json"))
139
+ if len(cache_files) <= self.max_entries:
140
+ return
141
+ # Sort by mtime, remove oldest
142
+ cache_files.sort(key=lambda p: p.stat().st_mtime)
143
+ to_remove = cache_files[: len(cache_files) - self.max_entries]
144
+ for f in to_remove:
145
+ f.unlink(missing_ok=True)
146
+ logger.debug("Evicted %d cache entries (limit: %d)", len(to_remove), self.max_entries)
147
+
148
+ def cleanup_expired(self) -> int:
149
+ """
150
+ Remove expired cache entries.
151
+
152
+ Returns:
153
+ Number of expired entries removed
154
+ """
155
+ if not self.enabled or not self.cache_dir.exists():
156
+ return 0
157
+
158
+ count = 0
159
+ for cache_file in self.cache_dir.glob("*.json"):
160
+ try:
161
+ with open(cache_file) as f:
162
+ data = json.load(f)
163
+ if time.time() - data.get("timestamp", 0) > self.ttl:
164
+ cache_file.unlink()
165
+ count += 1
166
+ except (json.JSONDecodeError, OSError):
167
+ # Remove corrupted cache files
168
+ cache_file.unlink(missing_ok=True)
169
+ count += 1
170
+
171
+ if count > 0:
172
+ logger.debug("Cleaned up %d expired cache entries", count)
173
+ return count