aibar 0.53.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.
aibar/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ @file
3
+ @brief Package metadata for aibar.
4
+ @details Exposes the package version for the multi-provider usage monitoring application.
5
+ """
6
+
7
+ __version__ = "0.53.0"
aibar/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ @file
3
+ @brief Module execution entry point for aibar.
4
+ @details Delegates to aibar.cli:main to enable `python -m aibar` invocation.
5
+ @satisfies REQ-024
6
+ """
7
+
8
+ from aibar.cli import main
9
+
10
+ if __name__ == "__main__":
11
+ main() # pyright: ignore[reportCallIssue]
aibar/cache.py ADDED
@@ -0,0 +1,352 @@
1
+ """
2
+ @file
3
+ @brief Provider result caching primitives.
4
+ @details Implements in-memory and disk cache entries, TTL invalidation, and raw-payload sanitization for provider metrics.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from datetime import datetime, timedelta, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel
14
+
15
+ from aibar.providers.base import ProviderName, ProviderResult, WindowPeriod
16
+
17
+
18
+ class CacheEntry(BaseModel):
19
+ """
20
+ @brief Define cache entry component.
21
+ @details Encapsulates cache entry state and operations for AIBar runtime flows with deterministic behavior and explicit interfaces.
22
+ """
23
+
24
+ result: ProviderResult
25
+ cached_at: datetime
26
+ ttl_seconds: int
27
+
28
+ def is_expired(self) -> bool:
29
+ """
30
+ @brief Execute is expired.
31
+ @details Applies is expired logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
32
+ @return {bool} Function return value.
33
+ """
34
+ now = datetime.now(timezone.utc)
35
+ cached_at_utc = self.cached_at.replace(tzinfo=timezone.utc)
36
+ return now > cached_at_utc + timedelta(seconds=self.ttl_seconds)
37
+
38
+
39
+ class ResultCache:
40
+ """
41
+ @brief Define result cache component.
42
+ @details Encapsulates result cache state and operations for AIBar runtime flows with deterministic behavior and explicit interfaces.
43
+ """
44
+
45
+ DEFAULT_TTL = 120 # 2 minutes
46
+ PROVIDER_TTLS = {
47
+ ProviderName.OPENAI: 180, # 3 minutes - usage data is historical
48
+ ProviderName.OPENROUTER: 180, # 3 minutes - credits update periodically
49
+ ProviderName.COPILOT: 300, # 5 minutes - reports are slow to update
50
+ }
51
+ RATE_LIMIT_COOLDOWN = 30 # seconds to wait before retrying after HTTP 429
52
+
53
+ def __init__(self, cache_dir: Path | None = None) -> None:
54
+ """
55
+ @brief Execute init.
56
+ @details Applies init logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
57
+ @param cache_dir {Path | None} Input parameter `cache_dir`.
58
+ @return {None} Function return value.
59
+ """
60
+ self._memory_cache: dict[str, CacheEntry] = {}
61
+ self._cache_dir = cache_dir or self._default_cache_dir()
62
+ self._ensure_cache_dir()
63
+
64
+ def _default_cache_dir(self) -> Path:
65
+ """
66
+ @brief Execute default cache dir.
67
+ @details Applies default cache dir logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
68
+ @return {Path} Function return value.
69
+ """
70
+ xdg_cache = os.environ.get("XDG_CACHE_HOME")
71
+ base = Path(xdg_cache) if xdg_cache else Path.home() / ".cache"
72
+ return base / "aibar"
73
+
74
+ def _ensure_cache_dir(self) -> None:
75
+ """
76
+ @brief Execute ensure cache dir.
77
+ @details Applies ensure cache dir logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
78
+ @return {None} Function return value.
79
+ """
80
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
81
+
82
+ def _cache_key(self, provider: ProviderName, window: WindowPeriod) -> str:
83
+ """
84
+ @brief Execute cache key.
85
+ @details Applies cache key logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
86
+ @param provider {ProviderName} Input parameter `provider`.
87
+ @param window {WindowPeriod} Input parameter `window`.
88
+ @return {str} Function return value.
89
+ """
90
+ return f"{provider.value}:{window.value}"
91
+
92
+ def _disk_path(self, provider: ProviderName, window: WindowPeriod) -> Path:
93
+ """
94
+ @brief Execute disk path.
95
+ @details Applies disk path logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
96
+ @param provider {ProviderName} Input parameter `provider`.
97
+ @param window {WindowPeriod} Input parameter `window`.
98
+ @return {Path} Function return value.
99
+ """
100
+ return self._cache_dir / f"{provider.value}_{window.value}.json"
101
+
102
+ def _is_cacheable_provider(self, provider: ProviderName) -> bool:
103
+ """
104
+ @brief Check whether result-cache read/write logic is enabled for a provider.
105
+ @details Returns True for all providers; the legacy result-cache surface is
106
+ no longer consumed by the shared show retrieval pipeline.
107
+ @param provider {ProviderName} Provider identifier to classify.
108
+ @return {bool} True when provider caching is enabled.
109
+ @satisfies CTN-004
110
+ """
111
+ return True
112
+
113
+ def get(self, provider: ProviderName, window: WindowPeriod) -> ProviderResult | None:
114
+ """
115
+ @brief Execute get.
116
+ @details Applies get logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
117
+ @param provider {ProviderName} Input parameter `provider`.
118
+ @param window {WindowPeriod} Input parameter `window`.
119
+ @return {ProviderResult | None} Function return value.
120
+ """
121
+ if not self._is_cacheable_provider(provider):
122
+ return None
123
+
124
+ key = self._cache_key(provider, window)
125
+
126
+ # Check memory cache first
127
+ if entry := self._memory_cache.get(key):
128
+ if not entry.is_expired():
129
+ return entry.result
130
+ del self._memory_cache[key]
131
+
132
+ # Fall back to disk cache
133
+ return self._load_from_disk(provider, window)
134
+
135
+ def set(self, result: ProviderResult) -> None:
136
+ """
137
+ @brief Execute set.
138
+ @details Applies set logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
139
+ @param result {ProviderResult} Input parameter `result`.
140
+ @return {None} Function return value.
141
+ """
142
+ if not self._is_cacheable_provider(result.provider):
143
+ self.invalidate(provider=result.provider, window=result.window)
144
+ self.clear_rate_limit(result.provider)
145
+ path = self._disk_path(result.provider, result.window)
146
+ try:
147
+ path.unlink(missing_ok=True)
148
+ except (OSError, IOError):
149
+ pass
150
+ return
151
+
152
+ key = self._cache_key(result.provider, result.window)
153
+ ttl = self.PROVIDER_TTLS.get(result.provider, self.DEFAULT_TTL)
154
+
155
+ entry = CacheEntry(
156
+ result=result,
157
+ cached_at=datetime.now(timezone.utc),
158
+ ttl_seconds=ttl,
159
+ )
160
+ self._memory_cache[key] = entry
161
+
162
+ # Persist to disk if successful; activate cooldown on 429
163
+ if not result.is_error:
164
+ self._save_to_disk(result)
165
+ self.clear_rate_limit(result.provider)
166
+ elif result.raw.get("status_code") == 429:
167
+ self.set_rate_limited(result.provider)
168
+
169
+ def get_last_good(self, provider: ProviderName, window: WindowPeriod) -> ProviderResult | None:
170
+ """
171
+ @brief Execute get last good.
172
+ @details Applies get last good logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
173
+ @param provider {ProviderName} Input parameter `provider`.
174
+ @param window {WindowPeriod} Input parameter `window`.
175
+ @return {ProviderResult | None} Function return value.
176
+ """
177
+ if not self._is_cacheable_provider(provider):
178
+ return None
179
+
180
+ return self._load_from_disk(provider, window, ignore_ttl=True)
181
+
182
+ def invalidate(
183
+ self, provider: ProviderName | None = None, window: WindowPeriod | None = None
184
+ ) -> None:
185
+ """
186
+ @brief Execute invalidate.
187
+ @details Applies invalidate logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
188
+ @param provider {ProviderName | None} Input parameter `provider`.
189
+ @param window {WindowPeriod | None} Input parameter `window`.
190
+ @return {None} Function return value.
191
+ """
192
+ if provider is None and window is None:
193
+ # Clear all
194
+ self._memory_cache.clear()
195
+ return
196
+
197
+ keys_to_remove = []
198
+ for key in self._memory_cache:
199
+ p, w = key.split(":")
200
+ if (provider is None or p == provider.value) and (window is None or w == window.value):
201
+ keys_to_remove.append(key)
202
+
203
+ for key in keys_to_remove:
204
+ del self._memory_cache[key]
205
+
206
+ def _cooldown_path(self, provider: ProviderName) -> Path:
207
+ """
208
+ @brief Resolve disk path for provider rate-limit cooldown marker.
209
+ @details Returns path under cache directory keyed by provider name.
210
+ @param provider {ProviderName} Provider to resolve cooldown path for.
211
+ @return {Path} Absolute path to cooldown marker file.
212
+ """
213
+ return self._cache_dir / f"{provider.value}_rate_limited"
214
+
215
+ def set_rate_limited(self, provider: ProviderName) -> None:
216
+ """
217
+ @brief Write rate-limit cooldown marker to disk for a provider.
218
+ @details Persists current UTC timestamp to cooldown file. Subsequent
219
+ is_rate_limited calls within RATE_LIMIT_COOLDOWN seconds return True.
220
+ Silently ignores disk write failures.
221
+ @param provider {ProviderName} Provider to mark as rate-limited.
222
+ @return {None} No return value.
223
+ """
224
+ if not self._is_cacheable_provider(provider):
225
+ return
226
+
227
+ path = self._cooldown_path(provider)
228
+ try:
229
+ with open(path, "w") as f:
230
+ f.write(str(datetime.now(timezone.utc).timestamp()))
231
+ except (OSError, IOError):
232
+ pass
233
+
234
+ def is_rate_limited(self, provider: ProviderName) -> bool:
235
+ """
236
+ @brief Check whether provider is in rate-limit cooldown period.
237
+ @details Reads timestamp from cooldown marker file. Returns True if age
238
+ is less than RATE_LIMIT_COOLDOWN seconds. Expired markers are deleted.
239
+ @param provider {ProviderName} Provider to check cooldown for.
240
+ @return {bool} True if provider is within cooldown period.
241
+ """
242
+ if not self._is_cacheable_provider(provider):
243
+ return False
244
+
245
+ path = self._cooldown_path(provider)
246
+ if not path.exists():
247
+ return False
248
+ try:
249
+ with open(path) as f:
250
+ ts = float(f.read().strip())
251
+ age = datetime.now(timezone.utc).timestamp() - ts
252
+ if age < self.RATE_LIMIT_COOLDOWN:
253
+ return True
254
+ path.unlink(missing_ok=True)
255
+ return False
256
+ except (OSError, IOError, ValueError):
257
+ return False
258
+
259
+ def clear_rate_limit(self, provider: ProviderName) -> None:
260
+ """
261
+ @brief Remove rate-limit cooldown marker for a provider.
262
+ @details Deletes cooldown marker file if present. Called on successful fetch.
263
+ @param provider {ProviderName} Provider to clear cooldown for.
264
+ @return {None} No return value.
265
+ """
266
+ path = self._cooldown_path(provider)
267
+ try:
268
+ path.unlink(missing_ok=True)
269
+ except (OSError, IOError):
270
+ pass
271
+
272
+ def _save_to_disk(self, result: ProviderResult) -> None:
273
+ """
274
+ @brief Execute save to disk.
275
+ @details Applies save to disk logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
276
+ @param result {ProviderResult} Input parameter `result`.
277
+ @return {None} Function return value.
278
+ """
279
+ path = self._disk_path(result.provider, result.window)
280
+ try:
281
+ # Sanitize raw data to remove any potential tokens
282
+ sanitized = result.model_copy(deep=True)
283
+ sanitized.raw = self._sanitize_raw(sanitized.raw)
284
+
285
+ with open(path, "w") as f:
286
+ f.write(sanitized.model_dump_json(indent=2))
287
+ except (OSError, IOError):
288
+ # Silently fail on disk write errors
289
+ pass
290
+
291
+ def _load_from_disk(
292
+ self,
293
+ provider: ProviderName,
294
+ window: WindowPeriod,
295
+ ignore_ttl: bool = False,
296
+ ) -> ProviderResult | None:
297
+ """
298
+ @brief Execute load from disk.
299
+ @details Applies load from disk logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
300
+ @param provider {ProviderName} Input parameter `provider`.
301
+ @param window {WindowPeriod} Input parameter `window`.
302
+ @param ignore_ttl {bool} Input parameter `ignore_ttl`.
303
+ @return {ProviderResult | None} Function return value.
304
+ """
305
+ path = self._disk_path(provider, window)
306
+ if not path.exists():
307
+ return None
308
+
309
+ try:
310
+ with open(path) as f:
311
+ data = json.load(f)
312
+ result = ProviderResult.model_validate(data)
313
+
314
+ if ignore_ttl:
315
+ return result
316
+
317
+ # Check if disk cache is still valid
318
+ ttl = self.PROVIDER_TTLS.get(provider, self.DEFAULT_TTL)
319
+ age = datetime.now(timezone.utc) - result.updated_at.replace(tzinfo=timezone.utc)
320
+ if age.total_seconds() <= ttl:
321
+ return result
322
+
323
+ return None
324
+ except (OSError, IOError, json.JSONDecodeError, ValueError):
325
+ return None
326
+
327
+ def _sanitize_raw(self, raw: dict[str, Any]) -> dict[str, Any]:
328
+ """
329
+ @brief Execute sanitize raw.
330
+ @details Applies sanitize raw logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
331
+ @param raw {dict[str, Any]} Input parameter `raw`.
332
+ @return {dict[str, Any]} Function return value.
333
+ """
334
+ sensitive_keys = {"token", "key", "secret", "password", "authorization"}
335
+
336
+ def clean(obj: Any) -> Any:
337
+ """
338
+ @brief Execute clean.
339
+ @details Applies clean logic for AIBar runtime behavior with explicit input/output contracts and deterministic side effects.
340
+ @param obj {Any} Input parameter `obj`.
341
+ @return {Any} Function return value.
342
+ """
343
+ if isinstance(obj, dict):
344
+ return {
345
+ k: "[REDACTED]" if k.lower() in sensitive_keys else clean(v)
346
+ for k, v in obj.items()
347
+ }
348
+ elif isinstance(obj, list):
349
+ return [clean(item) for item in obj]
350
+ return obj
351
+
352
+ return clean(raw)