codex-cli-usage 0.1.8__tar.gz → 0.1.10__tar.gz
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.
- codex_cli_usage-0.1.10/.github/workflows/publish.yml +29 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/PKG-INFO +6 -3
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/README.md +4 -1
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/pyproject.toml +1 -1
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/src/codex_cli_usage/__init__.py +44 -10
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/test_auth.py +9 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/test_usage.py +20 -1
- codex_cli_usage-0.1.10/uv.lock +8 -0
- codex_cli_usage-0.1.8/.github/workflows/publish.yml +0 -18
- codex_cli_usage-0.1.8/uv.lock +0 -20
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/.gitignore +0 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/src/codex_cli_usage/__main__.py +0 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/fixtures/duration_variants.json +0 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/fixtures/nullable_missing.json +0 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/fixtures/primary_5h_secondary_weekly.json +0 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/fixtures/primary_weekly_only.json +0 -0
- {codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/test_app_server.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v7.0.1
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: false
|
|
22
|
+
- name: Verify version tag
|
|
23
|
+
run: test "$GITHUB_REF_NAME" = "v$(uv --no-config version --short)"
|
|
24
|
+
- name: Test
|
|
25
|
+
run: uv --no-config run --locked python -m unittest discover -s tests
|
|
26
|
+
- name: Build
|
|
27
|
+
run: uv --no-config build --no-sources
|
|
28
|
+
- name: Publish
|
|
29
|
+
run: uv publish --trusted-publishing always
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codex-cli-usage
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: Codex CLI usage monitor — fetches rate limits through the Codex app-server
|
|
5
5
|
Project-URL: Homepage, https://github.com/wakamex/codex-cli-usage
|
|
6
6
|
Project-URL: Source, https://github.com/wakamex/codex-cli-usage
|
|
@@ -100,7 +100,10 @@ window actually classifies as 5-hour or weekly. Readers should prefer
|
|
|
100
100
|
|
|
101
101
|
### Authentication
|
|
102
102
|
|
|
103
|
-
The OAuth tokens live at `~/.codex/auth.json
|
|
103
|
+
The OAuth tokens live at `~/.codex/auth.json` by default and are written by the
|
|
104
|
+
Codex CLI on login. When `CODEX_HOME` is set, the direct compatibility fallback
|
|
105
|
+
reads `$CODEX_HOME/auth.json`. The preferred app-server path leaves credential
|
|
106
|
+
resolution to Codex.
|
|
104
107
|
|
|
105
108
|
Codex owns automatic token refresh and credential persistence. The compatibility
|
|
106
109
|
fallback reads `auth.json` but never modifies credentials.
|
|
@@ -91,7 +91,10 @@ window actually classifies as 5-hour or weekly. Readers should prefer
|
|
|
91
91
|
|
|
92
92
|
### Authentication
|
|
93
93
|
|
|
94
|
-
The OAuth tokens live at `~/.codex/auth.json
|
|
94
|
+
The OAuth tokens live at `~/.codex/auth.json` by default and are written by the
|
|
95
|
+
Codex CLI on login. When `CODEX_HOME` is set, the direct compatibility fallback
|
|
96
|
+
reads `$CODEX_HOME/auth.json`. The preferred app-server path leaves credential
|
|
97
|
+
resolution to Codex.
|
|
95
98
|
|
|
96
99
|
Codex owns automatic token refresh and credential persistence. The compatibility
|
|
97
100
|
fallback reads `auth.json` but never modifies credentials.
|
|
@@ -16,6 +16,8 @@ Usage:
|
|
|
16
16
|
|
|
17
17
|
import argparse
|
|
18
18
|
import json
|
|
19
|
+
import math
|
|
20
|
+
import os
|
|
19
21
|
import queue
|
|
20
22
|
import shutil
|
|
21
23
|
import signal
|
|
@@ -66,8 +68,15 @@ def _find_codex_path(filename: str) -> Path:
|
|
|
66
68
|
return native
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
def _find_codex_auth_path() -> Path:
|
|
72
|
+
codex_home = os.environ.get("CODEX_HOME")
|
|
73
|
+
if codex_home:
|
|
74
|
+
return Path(codex_home) / "auth.json"
|
|
75
|
+
return _find_codex_path("auth.json")
|
|
76
|
+
|
|
77
|
+
|
|
69
78
|
CODEX_DIR = Path.home() / ".codex"
|
|
70
|
-
AUTH_FILE =
|
|
79
|
+
AUTH_FILE = _find_codex_auth_path()
|
|
71
80
|
USAGE_FILE = _find_codex_path("usage-limits.json")
|
|
72
81
|
DAEMON_INTERVAL = 300 # 5 minutes
|
|
73
82
|
USAGE_URL = "https://chatgpt.com/backend-api/codex/usage"
|
|
@@ -323,7 +332,7 @@ def _fetch_usage_direct() -> dict:
|
|
|
323
332
|
"""
|
|
324
333
|
auth = get_auth()
|
|
325
334
|
if not auth:
|
|
326
|
-
raise RuntimeError("No auth at
|
|
335
|
+
raise RuntimeError(f"No auth at {AUTH_FILE}. Run `codex` first")
|
|
327
336
|
|
|
328
337
|
tokens = auth.get("tokens", {})
|
|
329
338
|
access_token = tokens.get("access_token")
|
|
@@ -381,13 +390,36 @@ def classify_window(seconds: int | float | None, kind: str) -> tuple[str, str]:
|
|
|
381
390
|
|
|
382
391
|
|
|
383
392
|
def _build_window(kind: str, window: dict) -> dict:
|
|
393
|
+
used_percent = window.get("used_percent")
|
|
384
394
|
seconds = window.get("limit_window_seconds")
|
|
385
|
-
key, label = classify_window(seconds, kind)
|
|
386
395
|
reset_at = window.get("reset_at")
|
|
396
|
+
for value in (used_percent, seconds, reset_at):
|
|
397
|
+
if value is not None and (
|
|
398
|
+
not isinstance(value, int | float)
|
|
399
|
+
or isinstance(value, bool)
|
|
400
|
+
or (isinstance(value, float) and not math.isfinite(value))
|
|
401
|
+
):
|
|
402
|
+
raise UsageServiceError("Invalid rate limit response")
|
|
403
|
+
if seconds is not None and seconds <= 0:
|
|
404
|
+
raise UsageServiceError("Invalid rate limit response")
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
key, label = classify_window(seconds, kind)
|
|
408
|
+
except OverflowError:
|
|
409
|
+
raise UsageServiceError("Invalid rate limit response") from None
|
|
410
|
+
try:
|
|
411
|
+
resets_at = (
|
|
412
|
+
datetime.fromtimestamp(reset_at, tz=timezone.utc).isoformat()
|
|
413
|
+
if reset_at is not None
|
|
414
|
+
else None
|
|
415
|
+
)
|
|
416
|
+
except (OSError, OverflowError, ValueError):
|
|
417
|
+
raise UsageServiceError("Invalid rate limit response") from None
|
|
418
|
+
|
|
387
419
|
return {
|
|
388
420
|
"kind": kind,
|
|
389
|
-
"pct":
|
|
390
|
-
"resets_at":
|
|
421
|
+
"pct": used_percent,
|
|
422
|
+
"resets_at": resets_at,
|
|
391
423
|
"window_secs": seconds,
|
|
392
424
|
"key": key,
|
|
393
425
|
"label": label,
|
|
@@ -442,11 +474,13 @@ def build_usage_json(api_data: dict) -> dict:
|
|
|
442
474
|
# Code review limits
|
|
443
475
|
cr = api_data.get("code_review_rate_limit") or {}
|
|
444
476
|
cr_primary = cr.get("primary_window")
|
|
445
|
-
if cr_primary
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
477
|
+
if isinstance(cr_primary, dict):
|
|
478
|
+
window = _build_window("code_review", cr_primary)
|
|
479
|
+
if window["pct"] is not None and window["pct"] > 0:
|
|
480
|
+
result["code_review"] = {
|
|
481
|
+
"pct": window["pct"],
|
|
482
|
+
"resets_at": window["resets_at"],
|
|
483
|
+
}
|
|
450
484
|
|
|
451
485
|
# Credits
|
|
452
486
|
credits = api_data.get("credits") or {}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import tempfile
|
|
3
4
|
import unittest
|
|
4
5
|
import urllib.error
|
|
@@ -65,6 +66,14 @@ class AuthenticationRecoveryTests(unittest.TestCase):
|
|
|
65
66
|
def write_auth(self, auth):
|
|
66
67
|
self.auth_file.write_text(json.dumps(auth))
|
|
67
68
|
|
|
69
|
+
def test_codex_home_sets_direct_auth_path(self):
|
|
70
|
+
with tempfile.TemporaryDirectory() as tmp, patch.dict(
|
|
71
|
+
os.environ, {"CODEX_HOME": tmp}, clear=True
|
|
72
|
+
):
|
|
73
|
+
self.assertEqual(
|
|
74
|
+
codex_cli_usage._find_codex_auth_path(), Path(tmp) / "auth.json"
|
|
75
|
+
)
|
|
76
|
+
|
|
68
77
|
def test_transient_403_retries_same_token_without_refresh(self):
|
|
69
78
|
requests = []
|
|
70
79
|
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from unittest.mock import patch
|
|
7
7
|
|
|
8
8
|
from codex_cli_usage import (
|
|
9
|
+
UsageServiceError,
|
|
9
10
|
build_usage_json,
|
|
10
11
|
classify_window,
|
|
11
12
|
cmd_status,
|
|
@@ -86,6 +87,20 @@ class UsageWindowTests(unittest.TestCase):
|
|
|
86
87
|
"window_secs": None,
|
|
87
88
|
})
|
|
88
89
|
|
|
90
|
+
def test_malformed_window_values_are_unavailable(self):
|
|
91
|
+
invalid_windows = (
|
|
92
|
+
{"used_percent": float("nan")},
|
|
93
|
+
{"used_percent": 1, "limit_window_seconds": float("inf")},
|
|
94
|
+
{"used_percent": 1, "reset_at": "not-a-timestamp"},
|
|
95
|
+
{"used_percent": 1, "reset_at": 10**30},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
for window in invalid_windows:
|
|
99
|
+
with self.subTest(window=window), self.assertRaisesRegex(
|
|
100
|
+
UsageServiceError, "Invalid rate limit response"
|
|
101
|
+
):
|
|
102
|
+
build_usage_json({"rate_limit": {"primary_window": window}})
|
|
103
|
+
|
|
89
104
|
def test_status_shows_one_weekly_row_and_no_spark_bucket(self):
|
|
90
105
|
output = StringIO()
|
|
91
106
|
with patch(
|
|
@@ -101,7 +116,11 @@ class UsageWindowTests(unittest.TestCase):
|
|
|
101
116
|
def test_statusline_enumerates_structured_windows(self):
|
|
102
117
|
usage = build_usage_json(fixture("primary_5h_secondary_weekly.json"))
|
|
103
118
|
output = StringIO()
|
|
104
|
-
with
|
|
119
|
+
with (
|
|
120
|
+
patch("codex_cli_usage._get_cached_usage", return_value=usage),
|
|
121
|
+
patch("codex_cli_usage._TTY", False),
|
|
122
|
+
redirect_stdout(output),
|
|
123
|
+
):
|
|
105
124
|
cmd_statusline()
|
|
106
125
|
|
|
107
126
|
self.assertIn("5h:12%", output.getvalue())
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
name: Publish to PyPI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
id-token: write
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
publish:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
environment: pypi
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v4
|
|
16
|
-
- uses: astral-sh/setup-uv@v4
|
|
17
|
-
- run: uv build
|
|
18
|
-
- uses: pypa/gh-action-pypi-publish@release/v1
|
codex_cli_usage-0.1.8/uv.lock
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
version = 1
|
|
2
|
-
revision = 3
|
|
3
|
-
requires-python = ">=3.12"
|
|
4
|
-
|
|
5
|
-
[options]
|
|
6
|
-
exclude-newer = "2026-07-05T20:06:09.848919716Z"
|
|
7
|
-
exclude-newer-span = "P7D"
|
|
8
|
-
|
|
9
|
-
[options.exclude-newer-package]
|
|
10
|
-
clanker-analytics = false
|
|
11
|
-
ghp = false
|
|
12
|
-
search-claude-history = false
|
|
13
|
-
gemini-cli-usage = false
|
|
14
|
-
ccusage = false
|
|
15
|
-
codex-cli-usage = false
|
|
16
|
-
|
|
17
|
-
[[package]]
|
|
18
|
-
name = "codex-cli-usage"
|
|
19
|
-
version = "0.1.8"
|
|
20
|
-
source = { editable = "." }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_cli_usage-0.1.8 → codex_cli_usage-0.1.10}/tests/fixtures/primary_5h_secondary_weekly.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|