codex-usage-tracking 0.3.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.
Files changed (50) hide show
  1. codex_usage_tracker/__init__.py +7 -0
  2. codex_usage_tracker/__main__.py +6 -0
  3. codex_usage_tracker/allowance.py +759 -0
  4. codex_usage_tracker/api_payloads.py +90 -0
  5. codex_usage_tracker/cli.py +1326 -0
  6. codex_usage_tracker/context.py +410 -0
  7. codex_usage_tracker/costing.py +176 -0
  8. codex_usage_tracker/dashboard.py +389 -0
  9. codex_usage_tracker/diagnostics.py +624 -0
  10. codex_usage_tracker/formatting.py +225 -0
  11. codex_usage_tracker/json_contracts.py +350 -0
  12. codex_usage_tracker/mcp_server.py +371 -0
  13. codex_usage_tracker/models.py +92 -0
  14. codex_usage_tracker/parser.py +491 -0
  15. codex_usage_tracker/paths.py +18 -0
  16. codex_usage_tracker/plugin_data/__init__.py +1 -0
  17. codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
  18. codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
  19. codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
  20. codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
  21. codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
  22. codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
  23. codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
  24. codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
  25. codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
  26. codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
  27. codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
  28. codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
  29. codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
  30. codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
  31. codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
  32. codex_usage_tracker/plugin_installer.py +312 -0
  33. codex_usage_tracker/pricing.py +57 -0
  34. codex_usage_tracker/pricing_config.py +223 -0
  35. codex_usage_tracker/pricing_estimates.py +44 -0
  36. codex_usage_tracker/pricing_openai.py +253 -0
  37. codex_usage_tracker/projects.py +347 -0
  38. codex_usage_tracker/recommendations.py +270 -0
  39. codex_usage_tracker/reports.py +637 -0
  40. codex_usage_tracker/schema.py +71 -0
  41. codex_usage_tracker/server.py +400 -0
  42. codex_usage_tracker/store.py +666 -0
  43. codex_usage_tracker/support.py +147 -0
  44. codex_usage_tracker/threads.py +183 -0
  45. codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
  46. codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
  47. codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
  48. codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
  49. codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
  50. codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,389 @@
1
+ """Static dashboard generation from aggregate-only usage rows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import html
7
+ import json
8
+ import os
9
+ import re
10
+ import shutil
11
+ from importlib import resources
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from codex_usage_tracker.allowance import (
16
+ annotate_rows_with_allowance,
17
+ load_allowance_config,
18
+ summarize_allowance_usage,
19
+ )
20
+ from codex_usage_tracker.paths import (
21
+ DEFAULT_ALLOWANCE_PATH,
22
+ DEFAULT_DASHBOARD_PATH,
23
+ DEFAULT_PRICING_PATH,
24
+ DEFAULT_PROJECTS_PATH,
25
+ DEFAULT_RATE_CARD_PATH,
26
+ DEFAULT_THRESHOLDS_PATH,
27
+ )
28
+ from codex_usage_tracker.pricing import annotate_rows_with_efficiency, load_pricing_config
29
+ from codex_usage_tracker.projects import (
30
+ annotate_rows_with_project_identity,
31
+ apply_project_privacy_to_rows,
32
+ load_project_config,
33
+ project_privacy_metadata,
34
+ validate_privacy_mode,
35
+ )
36
+ from codex_usage_tracker.recommendations import (
37
+ annotate_rows_with_recommendations,
38
+ load_threshold_config,
39
+ )
40
+ from codex_usage_tracker.store import (
41
+ query_dashboard_event_count,
42
+ query_dashboard_events,
43
+ refresh_metadata,
44
+ )
45
+ from codex_usage_tracker.threads import annotate_thread_attachments
46
+
47
+
48
+ def dashboard_payload(
49
+ db_path: Path,
50
+ limit: int | None = 5000,
51
+ offset: int = 0,
52
+ pricing_path: Path = DEFAULT_PRICING_PATH,
53
+ allowance_path: Path = DEFAULT_ALLOWANCE_PATH,
54
+ rate_card_path: Path = DEFAULT_RATE_CARD_PATH,
55
+ since: str | None = None,
56
+ api_token: str | None = None,
57
+ context_api_enabled: bool = False,
58
+ thresholds_path: Path = DEFAULT_THRESHOLDS_PATH,
59
+ projects_path: Path = DEFAULT_PROJECTS_PATH,
60
+ privacy_mode: str = "normal",
61
+ include_archived: bool = False,
62
+ ) -> dict[str, object]:
63
+ """Return aggregate-only dashboard data without rendering HTML."""
64
+
65
+ privacy_mode = validate_privacy_mode(privacy_mode)
66
+ normalized_offset = _normalize_offset(offset)
67
+ rows = annotate_thread_attachments(
68
+ query_dashboard_events(
69
+ db_path=db_path,
70
+ limit=limit,
71
+ offset=normalized_offset,
72
+ since=since,
73
+ include_archived=include_archived,
74
+ )
75
+ )
76
+ pricing = load_pricing_config(pricing_path)
77
+ allowance = load_allowance_config(allowance_path, rate_card_path=rate_card_path)
78
+ thresholds = load_threshold_config(thresholds_path)
79
+ projects = load_project_config(projects_path)
80
+ annotated_rows = annotate_rows_with_allowance(
81
+ annotate_rows_with_efficiency(rows, pricing),
82
+ allowance,
83
+ )
84
+ annotated_rows = annotate_rows_with_recommendations(annotated_rows, thresholds)
85
+ annotated_rows = annotate_rows_with_project_identity(annotated_rows, projects)
86
+ annotated_rows = apply_project_privacy_to_rows(annotated_rows, privacy_mode=privacy_mode)
87
+ allowance_summary = summarize_allowance_usage(annotated_rows, allowance)
88
+ normalized_limit = _normalize_limit(limit)
89
+ total_available_rows = query_dashboard_event_count(
90
+ db_path=db_path,
91
+ since=since,
92
+ include_archived=include_archived,
93
+ )
94
+ active_available_rows = query_dashboard_event_count(
95
+ db_path=db_path,
96
+ since=since,
97
+ include_archived=False,
98
+ )
99
+ all_history_available_rows = query_dashboard_event_count(
100
+ db_path=db_path,
101
+ since=since,
102
+ include_archived=True,
103
+ )
104
+ metadata = refresh_metadata(db_path)
105
+ parser_diagnostics = {
106
+ key.removeprefix("parser_"): _safe_int(value)
107
+ for key, value in metadata.items()
108
+ if key.startswith("parser_") and _safe_int(value)
109
+ }
110
+ return {
111
+ "rows": annotated_rows,
112
+ "pricing_configured": pricing.loaded and not pricing.error,
113
+ "pricing_source": pricing.source,
114
+ "pricing_snapshot": _pricing_snapshot(pricing.loaded, pricing.source, pricing.models),
115
+ "allowance_configured": allowance.loaded and not allowance.error,
116
+ "allowance_source": allowance_summary["source"],
117
+ "allowance_windows": allowance_summary["windows"],
118
+ "allowance_error": allowance_summary["error"],
119
+ "rate_card_configured": allowance_summary["rate_card_loaded"],
120
+ "rate_card_error": allowance_summary["rate_card_error"],
121
+ "loaded_row_count": len(rows),
122
+ "total_available_rows": total_available_rows,
123
+ "active_available_rows": active_available_rows,
124
+ "all_history_available_rows": all_history_available_rows,
125
+ "archived_available_rows": max(all_history_available_rows - active_available_rows, 0),
126
+ "include_archived": include_archived,
127
+ "history_scope": "all-history" if include_archived else "active",
128
+ "limit": normalized_limit,
129
+ "offset": normalized_offset,
130
+ "has_more": (
131
+ normalized_limit is not None
132
+ and normalized_offset + len(rows) < total_available_rows
133
+ ),
134
+ "next_offset": (
135
+ normalized_offset + len(rows)
136
+ if normalized_limit is not None
137
+ and normalized_offset + len(rows) < total_available_rows
138
+ else None
139
+ ),
140
+ "limit_label": "All" if normalized_limit is None else str(normalized_limit),
141
+ "parser_diagnostics": parser_diagnostics,
142
+ "parser_adapter": metadata.get("parser_adapter"),
143
+ "api_token": api_token or "",
144
+ "context_api_enabled": context_api_enabled,
145
+ "action_thresholds": thresholds.thresholds,
146
+ "thresholds_configured": thresholds.loaded and not thresholds.error,
147
+ "thresholds_error": thresholds.error,
148
+ "project_configured": projects.loaded and not projects.error,
149
+ "project_config_error": projects.error,
150
+ "privacy_mode": privacy_mode,
151
+ "project_metadata_privacy": project_privacy_metadata(privacy_mode),
152
+ }
153
+
154
+
155
+ def generate_dashboard(
156
+ db_path: Path,
157
+ output_path: Path = DEFAULT_DASHBOARD_PATH,
158
+ limit: int | None = 5000,
159
+ pricing_path: Path = DEFAULT_PRICING_PATH,
160
+ allowance_path: Path = DEFAULT_ALLOWANCE_PATH,
161
+ rate_card_path: Path = DEFAULT_RATE_CARD_PATH,
162
+ since: str | None = None,
163
+ api_token: str | None = None,
164
+ context_api_enabled: bool = False,
165
+ thresholds_path: Path = DEFAULT_THRESHOLDS_PATH,
166
+ projects_path: Path = DEFAULT_PROJECTS_PATH,
167
+ privacy_mode: str = "normal",
168
+ include_archived: bool = False,
169
+ ) -> Path:
170
+ output_path.parent.mkdir(parents=True, exist_ok=True)
171
+ guide_href = _dashboard_guide_href(output_path)
172
+ asset_base = _dashboard_assets_href(output_path)
173
+ stylesheet_href = _versioned_asset_href(output_path, asset_base, "dashboard.css")
174
+ format_script_src = _versioned_asset_href(output_path, asset_base, "dashboard_format.js")
175
+ data_script_src = _versioned_asset_href(output_path, asset_base, "dashboard_data.js")
176
+ state_script_src = _versioned_asset_href(output_path, asset_base, "dashboard_state.js")
177
+ script_src = _versioned_asset_href(output_path, asset_base, "dashboard.js")
178
+ previous_payload = _previous_dashboard_payload(output_path)
179
+ payload_dict = dashboard_payload(
180
+ db_path=db_path,
181
+ limit=limit,
182
+ pricing_path=pricing_path,
183
+ allowance_path=allowance_path,
184
+ rate_card_path=rate_card_path,
185
+ since=since,
186
+ api_token=api_token,
187
+ context_api_enabled=context_api_enabled,
188
+ thresholds_path=thresholds_path,
189
+ projects_path=projects_path,
190
+ privacy_mode=privacy_mode,
191
+ include_archived=include_archived,
192
+ )
193
+ payload_dict["pricing_snapshot_warning"] = _pricing_snapshot_warning(
194
+ previous_payload, payload_dict
195
+ )
196
+ payload = json.dumps(payload_dict, ensure_ascii=True).replace("</", "<\\/")
197
+ output_path.write_text(
198
+ _html(
199
+ payload,
200
+ guide_href=guide_href,
201
+ stylesheet_href=stylesheet_href,
202
+ format_script_src=format_script_src,
203
+ data_script_src=data_script_src,
204
+ state_script_src=state_script_src,
205
+ script_src=script_src,
206
+ ),
207
+ encoding="utf-8",
208
+ )
209
+ return output_path
210
+
211
+
212
+ def _normalize_limit(limit: int | None) -> int | None:
213
+ if limit is None or limit <= 0:
214
+ return None
215
+ return int(limit)
216
+
217
+
218
+ def _normalize_offset(offset: int | None) -> int:
219
+ if offset is None or offset <= 0:
220
+ return 0
221
+ return int(offset)
222
+
223
+
224
+ def _pricing_snapshot(
225
+ loaded: bool,
226
+ source: dict[str, Any] | None,
227
+ models: dict[str, dict[str, float]],
228
+ ) -> dict[str, Any]:
229
+ if not loaded:
230
+ return {"configured": False, "fingerprint": None}
231
+ public_source = {
232
+ key: value
233
+ for key, value in (source or {}).items()
234
+ if key
235
+ in {
236
+ "name",
237
+ "url",
238
+ "tier",
239
+ "fetched_at",
240
+ "model_count",
241
+ "official_model_count",
242
+ "estimated_model_count",
243
+ "pinned",
244
+ "pinned_at",
245
+ }
246
+ }
247
+ public_source.setdefault("model_count", len(models))
248
+ rates_fingerprint = hashlib.sha256(
249
+ json.dumps(models, sort_keys=True, ensure_ascii=True).encode("utf-8")
250
+ ).hexdigest()[:12]
251
+ fingerprint = hashlib.sha256(
252
+ json.dumps(
253
+ {**public_source, "rates_fingerprint": rates_fingerprint},
254
+ sort_keys=True,
255
+ ensure_ascii=True,
256
+ ).encode("utf-8")
257
+ ).hexdigest()[:12]
258
+ return {
259
+ "configured": True,
260
+ "fingerprint": fingerprint,
261
+ "rates_fingerprint": rates_fingerprint,
262
+ **public_source,
263
+ }
264
+
265
+
266
+ def _pricing_snapshot_warning(
267
+ previous_payload: dict[str, Any] | None, current_payload: dict[str, object]
268
+ ) -> str | None:
269
+ if not previous_payload:
270
+ return None
271
+ previous = previous_payload.get("pricing_snapshot")
272
+ current = current_payload.get("pricing_snapshot")
273
+ if not isinstance(previous, dict) or not isinstance(current, dict):
274
+ return None
275
+ previous_fingerprint = previous.get("fingerprint")
276
+ current_fingerprint = current.get("fingerprint")
277
+ if not previous_fingerprint or not current_fingerprint:
278
+ return None
279
+ if previous_fingerprint == current_fingerprint:
280
+ return None
281
+ previous_label = previous.get("fetched_at") or previous.get("pinned_at") or previous_fingerprint
282
+ current_label = current.get("fetched_at") or current.get("pinned_at") or current_fingerprint
283
+ return f"Pricing snapshot changed since the previous dashboard render: {previous_label} -> {current_label}."
284
+
285
+
286
+ def _previous_dashboard_payload(output_path: Path) -> dict[str, Any] | None:
287
+ if not output_path.exists():
288
+ return None
289
+ try:
290
+ text = output_path.read_text(encoding="utf-8")
291
+ except OSError:
292
+ return None
293
+ match = _USAGE_DATA_RE.search(text)
294
+ if not match:
295
+ return None
296
+ try:
297
+ raw = json.loads(match.group("payload"))
298
+ except json.JSONDecodeError:
299
+ return None
300
+ return raw if isinstance(raw, dict) else None
301
+
302
+
303
+ def _dashboard_guide_href(output_path: Path) -> str | None:
304
+ override = os.environ.get("CODEX_USAGE_TRACKER_DOCS_URL")
305
+ if override:
306
+ return override
307
+ try:
308
+ docs_source = resources.files("codex_usage_tracker.plugin_data").joinpath("docs")
309
+ docs_target = output_path.parent / "codex-usage-tracker-guide"
310
+ if docs_target.exists():
311
+ shutil.rmtree(docs_target)
312
+ _copy_resource_tree(docs_source, docs_target)
313
+ except (FileNotFoundError, ModuleNotFoundError, OSError):
314
+ return None
315
+ return "codex-usage-tracker-guide/dashboard-guide.html"
316
+
317
+
318
+ def _dashboard_assets_href(output_path: Path) -> str:
319
+ assets_source = resources.files("codex_usage_tracker.plugin_data").joinpath("dashboard")
320
+ assets_target = output_path.parent / "codex-usage-tracker-assets"
321
+ if assets_target.exists():
322
+ shutil.rmtree(assets_target)
323
+ _copy_resource_tree(assets_source, assets_target)
324
+ return "codex-usage-tracker-assets"
325
+
326
+
327
+ def _versioned_asset_href(output_path: Path, asset_base: str, filename: str) -> str:
328
+ asset_path = output_path.parent / asset_base / filename
329
+ try:
330
+ digest = hashlib.sha256(asset_path.read_bytes()).hexdigest()[:12]
331
+ except OSError:
332
+ return f"{asset_base}/{filename}"
333
+ return f"{asset_base}/{filename}?v={digest}"
334
+
335
+
336
+ def _copy_resource_tree(source: Any, target: Path) -> None:
337
+ target.mkdir(parents=True, exist_ok=True)
338
+ for child in source.iterdir():
339
+ destination = target / child.name
340
+ if child.is_dir():
341
+ _copy_resource_tree(child, destination)
342
+ else:
343
+ destination.write_bytes(child.read_bytes())
344
+
345
+
346
+ def _html(
347
+ payload: str,
348
+ guide_href: str | None = None,
349
+ *,
350
+ stylesheet_href: str = "codex-usage-tracker-assets/dashboard.css",
351
+ format_script_src: str = "codex-usage-tracker-assets/dashboard_format.js",
352
+ data_script_src: str = "codex-usage-tracker-assets/dashboard_data.js",
353
+ state_script_src: str = "codex-usage-tracker-assets/dashboard_state.js",
354
+ script_src: str = "codex-usage-tracker-assets/dashboard.js",
355
+ ) -> str:
356
+ template = _read_dashboard_asset("dashboard_template.html")
357
+ guide_link = (
358
+ f'<a class="guide-link" href="{html.escape(guide_href, quote=True)}">Dashboard guide</a>'
359
+ if guide_href
360
+ else ""
361
+ )
362
+ return (
363
+ template.replace("__TITLE__", html.escape("Codex Usage Dashboard"))
364
+ .replace("__STYLESHEET_HREF__", html.escape(stylesheet_href, quote=True))
365
+ .replace("__GUIDE_LINK__", guide_link)
366
+ .replace("__PAYLOAD__", payload)
367
+ .replace("__FORMAT_SCRIPT_SRC__", html.escape(format_script_src, quote=True))
368
+ .replace("__DATA_SCRIPT_SRC__", html.escape(data_script_src, quote=True))
369
+ .replace("__STATE_SCRIPT_SRC__", html.escape(state_script_src, quote=True))
370
+ .replace("__SCRIPT_SRC__", html.escape(script_src, quote=True))
371
+ )
372
+
373
+
374
+ def _read_dashboard_asset(name: str) -> str:
375
+ asset = resources.files("codex_usage_tracker.plugin_data").joinpath("dashboard", name)
376
+ return asset.read_text(encoding="utf-8")
377
+
378
+
379
+ def _safe_int(value: object) -> int:
380
+ try:
381
+ return int(str(value))
382
+ except (TypeError, ValueError):
383
+ return 0
384
+
385
+
386
+ _USAGE_DATA_RE = re.compile(
387
+ r'<script id="usage-data" type="application/json">(?P<payload>.*?)</script>',
388
+ re.DOTALL,
389
+ )