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 +7 -0
- aibar/__main__.py +11 -0
- aibar/cache.py +352 -0
- aibar/cli.py +4707 -0
- aibar/config.py +872 -0
- aibar/gnome-extension/aibar@aibar.panel/extension.js +2824 -0
- aibar/gnome-extension/aibar@aibar.panel/metadata.json +13 -0
- aibar/gnome-extension/aibar@aibar.panel/stylesheet.css +407 -0
- aibar/providers/__init__.py +27 -0
- aibar/providers/agy.py +315 -0
- aibar/providers/base.py +194 -0
- aibar/providers/codex.py +452 -0
- aibar/providers/copilot.py +614 -0
- aibar/providers/geminiai.py +1045 -0
- aibar/providers/openai_usage.py +246 -0
- aibar/providers/openrouter.py +266 -0
- aibar/providers/zai.py +400 -0
- aibar-0.53.0.dist-info/METADATA +370 -0
- aibar-0.53.0.dist-info/RECORD +23 -0
- aibar-0.53.0.dist-info/WHEEL +4 -0
- aibar-0.53.0.dist-info/entry_points.txt +2 -0
- aibar-0.53.0.dist-info/licenses/LICENSE +674 -0
- aibar-0.53.0.dist-info/licenses/LICENSE_GnomeCodexBar +201 -0
aibar/__init__.py
ADDED
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)
|