ccusage 0.1.12__tar.gz → 0.2.0__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.
- ccusage-0.2.0/.github/workflows/publish.yml +29 -0
- ccusage-0.2.0/.github/workflows/release-eligibility.yml +12 -0
- ccusage-0.2.0/LICENSE +21 -0
- ccusage-0.1.12/README.md → ccusage-0.2.0/PKG-INFO +14 -0
- ccusage-0.1.12/PKG-INFO → ccusage-0.2.0/README.md +3 -9
- {ccusage-0.1.12 → ccusage-0.2.0}/pyproject.toml +3 -1
- ccusage-0.2.0/release-notes/v0.2.0.md +20 -0
- {ccusage-0.1.12 → ccusage-0.2.0}/src/ccusage/__init__.py +57 -12
- {ccusage-0.1.12 → ccusage-0.2.0}/tests/test_ccusage.py +73 -0
- {ccusage-0.1.12 → ccusage-0.2.0}/uv.lock +1 -1
- ccusage-0.1.12/.github/workflows/publish.yml +0 -29
- {ccusage-0.1.12 → ccusage-0.2.0}/.gitignore +0 -0
- {ccusage-0.1.12 → ccusage-0.2.0}/.python-version +0 -0
- {ccusage-0.1.12 → ccusage-0.2.0}/src/ccusage/__main__.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
validate:
|
|
12
|
+
uses: wakamex/release-actions/.github/workflows/python-validation.yml@stable
|
|
13
|
+
|
|
14
|
+
publish:
|
|
15
|
+
needs: validate
|
|
16
|
+
runs-on: ubuntu-24.04
|
|
17
|
+
environment: pypi
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
id-token: write
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
|
+
- uses: wakamex/release-actions/pypi-publish@stable
|
|
24
|
+
|
|
25
|
+
github-release:
|
|
26
|
+
needs: publish
|
|
27
|
+
permissions:
|
|
28
|
+
contents: write
|
|
29
|
+
uses: wakamex/release-actions/.github/workflows/github-release.yml@stable
|
ccusage-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wakamex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ccusage
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Claude Code usage monitor — fetches rate limits from Anthropic's API
|
|
5
|
+
Project-URL: Homepage, https://github.com/wakamex/ccusage
|
|
6
|
+
Project-URL: Source, https://github.com/wakamex/ccusage
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
1
12
|
# ccusage
|
|
2
13
|
|
|
3
14
|
Claude Code usage monitor. Fetches your real rate limit data from Anthropic's API and displays it in the Claude Code statusline.
|
|
@@ -50,6 +61,8 @@ Configure the statusline in `~/.claude/settings.json`:
|
|
|
50
61
|
If Claude Code uses `CLAUDE_CONFIG_DIR`, ccusage reads credentials and writes
|
|
51
62
|
`usage-limits.json` in that directory instead.
|
|
52
63
|
|
|
64
|
+
Each refresh also saves a copy under `usage-limits/<account uuid>.json`, tagged with the logged-in account's UUID and email from Claude Code's `.claude.json`. If you switch between Claude accounts with `/login`, the account you logged out of keeps its last usage there. An idle account cannot use quota, so that snapshot stays accurate until its windows reset.
|
|
65
|
+
|
|
53
66
|
## Commands
|
|
54
67
|
|
|
55
68
|
| Command | Description |
|
|
@@ -177,4 +190,5 @@ seven_day: 75% at 60%, 50% at 35%, 25% at 15%
|
|
|
177
190
|
| `~/.claude/.credentials.json` | Claude Code | OAuth tokens, plan tier |
|
|
178
191
|
| `~/.claude/stats-cache.json` | Claude Code | Local usage stats (message counts, token counts per model) |
|
|
179
192
|
| `~/.claude/usage-limits.json` | ccusage daemon | Cached API usage data (this tool) |
|
|
193
|
+
| `~/.claude/usage-limits/<account uuid>.json` | ccusage daemon | Last usage seen for each Claude account (this tool) |
|
|
180
194
|
| `~/.claude/statsig/` | Claude Code | Feature flags, experiment assignments |
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: ccusage
|
|
3
|
-
Version: 0.1.12
|
|
4
|
-
Summary: Claude Code usage monitor — fetches rate limits from Anthropic's API
|
|
5
|
-
Project-URL: Homepage, https://github.com/wakamex/ccusage
|
|
6
|
-
Project-URL: Source, https://github.com/wakamex/ccusage
|
|
7
|
-
Requires-Python: >=3.12
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
1
|
# ccusage
|
|
11
2
|
|
|
12
3
|
Claude Code usage monitor. Fetches your real rate limit data from Anthropic's API and displays it in the Claude Code statusline.
|
|
@@ -59,6 +50,8 @@ Configure the statusline in `~/.claude/settings.json`:
|
|
|
59
50
|
If Claude Code uses `CLAUDE_CONFIG_DIR`, ccusage reads credentials and writes
|
|
60
51
|
`usage-limits.json` in that directory instead.
|
|
61
52
|
|
|
53
|
+
Each refresh also saves a copy under `usage-limits/<account uuid>.json`, tagged with the logged-in account's UUID and email from Claude Code's `.claude.json`. If you switch between Claude accounts with `/login`, the account you logged out of keeps its last usage there. An idle account cannot use quota, so that snapshot stays accurate until its windows reset.
|
|
54
|
+
|
|
62
55
|
## Commands
|
|
63
56
|
|
|
64
57
|
| Command | Description |
|
|
@@ -186,4 +179,5 @@ seven_day: 75% at 60%, 50% at 35%, 25% at 15%
|
|
|
186
179
|
| `~/.claude/.credentials.json` | Claude Code | OAuth tokens, plan tier |
|
|
187
180
|
| `~/.claude/stats-cache.json` | Claude Code | Local usage stats (message counts, token counts per model) |
|
|
188
181
|
| `~/.claude/usage-limits.json` | ccusage daemon | Cached API usage data (this tool) |
|
|
182
|
+
| `~/.claude/usage-limits/<account uuid>.json` | ccusage daemon | Last usage seen for each Claude account (this tool) |
|
|
189
183
|
| `~/.claude/statsig/` | Claude Code | Feature flags, experiment assignments |
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ccusage"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Claude Code usage monitor — fetches rate limits from Anthropic's API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
7
9
|
dependencies = []
|
|
8
10
|
|
|
9
11
|
[tool.uv]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
ccusage now remembers the usage of every Claude account you log into, so switching accounts with `/login` no longer loses the other account's numbers. The package is also licensed under MIT.
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Each refresh also saves a copy of the usage to `~/.claude/usage-limits/<account uuid>.json`, tagged with the logged-in account's UUID and email. The account you switched away from keeps its last usage there, and since an idle account can't use quota, that copy stays accurate until its windows reset. `usage-limits.json` also gains an `account` field and is otherwise unchanged. ([`e2ed247`](https://github.com/wakamex/ccusage/commit/e2ed247b0b677c7e36220cb5b83f916022e0d14b))
|
|
6
|
+
- If you switch accounts in the middle of a refresh, ccusage skips the per-account copy for that refresh, so usage is never saved under the wrong account. Both cache files are now written atomically, so readers never see a half-written file. ([`e2ed247`](https://github.com/wakamex/ccusage/commit/e2ed247b0b677c7e36220cb5b83f916022e0d14b))
|
|
7
|
+
- The wheel and sdist now include a LICENSE file and declare `License-Expression: MIT`. ([`adbcbdb`](https://github.com/wakamex/ccusage/commit/adbcbdb40450f0e231a46026ad00848b0d188ac1))
|
|
8
|
+
|
|
9
|
+
## Changes
|
|
10
|
+
|
|
11
|
+
- Create the GitHub Release from committed release notes ([`146bf16`](https://github.com/wakamex/ccusage/commit/146bf165297bdd16c7300bd0dfdac3240d568fbf))
|
|
12
|
+
- Use the shared release actions ([`e623b1d`](https://github.com/wakamex/ccusage/commit/e623b1d0f5189e71b561f1fbb5fda1c92b788069))
|
|
13
|
+
- Remove the Markdown fence from the release workflows ([`614db37`](https://github.com/wakamex/ccusage/commit/614db372050760ba84f34469acd2ade6e03b0206))
|
|
14
|
+
- Pin runners and actions, and follow release-actions stable ([`1ef57c9`](https://github.com/wakamex/ccusage/commit/1ef57c9009a6604b2bd0804ba654f84064a5b090))
|
|
15
|
+
- Keep the last usage of each Claude account ([`e2ed247`](https://github.com/wakamex/ccusage/commit/e2ed247b0b677c7e36220cb5b83f916022e0d14b))
|
|
16
|
+
- License ccusage under MIT ([`adbcbdb`](https://github.com/wakamex/ccusage/commit/adbcbdb40450f0e231a46026ad00848b0d188ac1))
|
|
17
|
+
|
|
18
|
+
## History
|
|
19
|
+
|
|
20
|
+
[v0.1.12...v0.2.0](https://github.com/wakamex/ccusage/compare/v0.1.12...v0.2.0)
|
|
@@ -9,6 +9,7 @@ Usage:
|
|
|
9
9
|
ccusage status Same as above
|
|
10
10
|
ccusage json Print raw JSON
|
|
11
11
|
ccusage daemon Run in foreground, refresh every 5 min, write to ~/.claude/usage-limits.json
|
|
12
|
+
and ~/.claude/usage-limits/<account uuid>.json
|
|
12
13
|
ccusage statusline Claude Code statusline command (reads stdin + cache)
|
|
13
14
|
ccusage install Print setup instructions
|
|
14
15
|
"""
|
|
@@ -77,6 +78,10 @@ def _resolve_claude_path(relative: str) -> Path:
|
|
|
77
78
|
CLAUDE_DIR = _claude_config_dir()
|
|
78
79
|
CREDENTIALS_FILE = _resolve_claude_path(".credentials.json")
|
|
79
80
|
USAGE_FILE = _resolve_claude_path("usage-limits.json")
|
|
81
|
+
# One snapshot per Claude account, keyed by account UUID, so an account that is
|
|
82
|
+
# logged out keeps its last usage. Idle accounts cannot use quota, so the
|
|
83
|
+
# snapshot stays accurate until its windows reset.
|
|
84
|
+
ACCOUNTS_DIR = USAGE_FILE.parent / "usage-limits"
|
|
80
85
|
DAEMON_INTERVAL = 300 # 5 minutes
|
|
81
86
|
UNAVAILABLE_RETRY_INTERVAL = 3600 # 1 hour
|
|
82
87
|
TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
|
|
@@ -447,15 +452,51 @@ def _unavailable_hint(data: dict) -> str | None:
|
|
|
447
452
|
return unavailable.get("hint") or unavailable.get("message") or "usage unavailable"
|
|
448
453
|
|
|
449
454
|
|
|
450
|
-
def _cache_unavailable(error: UsageUnavailableError) -> dict:
|
|
455
|
+
def _cache_unavailable(error: UsageUnavailableError, account: dict | None = None) -> dict:
|
|
451
456
|
data = build_unavailable_usage(error, get_plan(), _read_cache())
|
|
452
|
-
write_usage_file(data)
|
|
457
|
+
write_usage_file(data, account)
|
|
453
458
|
return data
|
|
454
459
|
|
|
455
460
|
|
|
456
|
-
def
|
|
457
|
-
"""
|
|
458
|
-
|
|
461
|
+
def current_account() -> dict | None:
|
|
462
|
+
"""Return the logged-in account's UUID and email from Claude Code's config."""
|
|
463
|
+
if "CLAUDE_CONFIG_DIR" in os.environ:
|
|
464
|
+
path = _claude_config_dir() / ".claude.json"
|
|
465
|
+
else:
|
|
466
|
+
path = Path.home() / ".claude.json"
|
|
467
|
+
try:
|
|
468
|
+
account = json.loads(path.read_text()).get("oauthAccount") or {}
|
|
469
|
+
except (OSError, ValueError, AttributeError):
|
|
470
|
+
return None
|
|
471
|
+
if not account.get("accountUuid"):
|
|
472
|
+
return None
|
|
473
|
+
return {"uuid": account["accountUuid"], "email": account.get("emailAddress")}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _write_json(path: Path, data: dict):
|
|
477
|
+
"""Write JSON through a temporary file so readers never see a partial file."""
|
|
478
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
479
|
+
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
480
|
+
try:
|
|
481
|
+
with os.fdopen(fd, "w") as file:
|
|
482
|
+
file.write(json.dumps(data, indent=2) + "\n")
|
|
483
|
+
os.replace(tmp_name, path)
|
|
484
|
+
except BaseException:
|
|
485
|
+
Path(tmp_name).unlink(missing_ok=True)
|
|
486
|
+
raise
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def write_usage_file(data: dict, account: dict | None = None):
|
|
490
|
+
"""Write usage data to ~/.claude/usage-limits.json and the account's snapshot.
|
|
491
|
+
|
|
492
|
+
`account` is the logged-in account read before the usage was fetched. The
|
|
493
|
+
snapshot is skipped when a /login switched accounts during the fetch, since
|
|
494
|
+
the data could then belong to either account.
|
|
495
|
+
"""
|
|
496
|
+
if account and current_account() == account:
|
|
497
|
+
data["account"] = account
|
|
498
|
+
_write_json(ACCOUNTS_DIR / f"{account['uuid']}.json", data)
|
|
499
|
+
_write_json(USAGE_FILE, data)
|
|
459
500
|
|
|
460
501
|
|
|
461
502
|
def _read_cache() -> dict | None:
|
|
@@ -493,11 +534,12 @@ def cmd_status(raw_json=False):
|
|
|
493
534
|
token) falls back to the last cached usage rather than crashing.
|
|
494
535
|
"""
|
|
495
536
|
stale = False
|
|
537
|
+
account = current_account()
|
|
496
538
|
try:
|
|
497
539
|
api_data = fetch_usage()
|
|
498
540
|
data = build_usage_json(api_data, get_plan())
|
|
499
541
|
except UsageUnavailableError as e:
|
|
500
|
-
data = _cache_unavailable(e)
|
|
542
|
+
data = _cache_unavailable(e, account)
|
|
501
543
|
except Exception as e:
|
|
502
544
|
cached = _read_cache()
|
|
503
545
|
if cached is None:
|
|
@@ -565,10 +607,11 @@ def cmd_refresh():
|
|
|
565
607
|
A one-shot equivalent of a single daemon tick — use it to force
|
|
566
608
|
~/.claude/usage-limits.json up to date without running the daemon.
|
|
567
609
|
"""
|
|
610
|
+
account = current_account()
|
|
568
611
|
try:
|
|
569
612
|
api_data = fetch_usage()
|
|
570
613
|
except UsageUnavailableError as e:
|
|
571
|
-
data = _cache_unavailable(e)
|
|
614
|
+
data = _cache_unavailable(e, account)
|
|
572
615
|
print(f"Updated {USAGE_FILE}")
|
|
573
616
|
print(f" Usage unavailable: {_unavailable_hint(data)}")
|
|
574
617
|
return
|
|
@@ -577,7 +620,7 @@ def cmd_refresh():
|
|
|
577
620
|
print(f"Could not refresh usage: {reason}", file=sys.stderr)
|
|
578
621
|
sys.exit(1)
|
|
579
622
|
data = build_usage_json(api_data, get_plan())
|
|
580
|
-
write_usage_file(data)
|
|
623
|
+
write_usage_file(data, account)
|
|
581
624
|
pcts = " ".join(f"{key}:{int(b['pct'])}%" for key, b in _quota_buckets(data))
|
|
582
625
|
print(f"Updated {USAGE_FILE}")
|
|
583
626
|
if pcts:
|
|
@@ -595,16 +638,17 @@ def cmd_daemon(interval: int = DAEMON_INTERVAL):
|
|
|
595
638
|
|
|
596
639
|
backoff = 0
|
|
597
640
|
while True:
|
|
641
|
+
account = current_account()
|
|
598
642
|
try:
|
|
599
643
|
api_data = fetch_usage()
|
|
600
644
|
plan = get_plan()
|
|
601
645
|
data = build_usage_json(api_data, plan)
|
|
602
|
-
write_usage_file(data)
|
|
646
|
+
write_usage_file(data, account)
|
|
603
647
|
backoff = 0
|
|
604
648
|
pcts = [f"{key}:{int(b['pct'])}%" for key, b in _quota_buckets(data)]
|
|
605
649
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] {' '.join(pcts)}")
|
|
606
650
|
except UsageUnavailableError as e:
|
|
607
|
-
data = _cache_unavailable(e)
|
|
651
|
+
data = _cache_unavailable(e, account)
|
|
608
652
|
backoff = UNAVAILABLE_RETRY_INTERVAL
|
|
609
653
|
print(
|
|
610
654
|
f"[{datetime.now().strftime('%H:%M:%S')}] "
|
|
@@ -634,13 +678,14 @@ def _get_cached_usage(max_age: int = DAEMON_INTERVAL) -> dict:
|
|
|
634
678
|
except Exception:
|
|
635
679
|
pass
|
|
636
680
|
# Cache is stale or missing — try to refresh
|
|
681
|
+
account = current_account()
|
|
637
682
|
try:
|
|
638
683
|
api_data = fetch_usage()
|
|
639
684
|
usage = build_usage_json(api_data, get_plan())
|
|
640
|
-
write_usage_file(usage)
|
|
685
|
+
write_usage_file(usage, account)
|
|
641
686
|
return usage
|
|
642
687
|
except UsageUnavailableError as e:
|
|
643
|
-
return _cache_unavailable(e)
|
|
688
|
+
return _cache_unavailable(e, account)
|
|
644
689
|
except Exception:
|
|
645
690
|
# Return whatever we had, even if stale
|
|
646
691
|
try:
|
|
@@ -464,5 +464,78 @@ class UnavailableCacheTests(unittest.TestCase):
|
|
|
464
464
|
self.assertNotIn("sess:", output)
|
|
465
465
|
|
|
466
466
|
|
|
467
|
+
class AccountSnapshotTests(unittest.TestCase):
|
|
468
|
+
def setUp(self):
|
|
469
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
470
|
+
self.addCleanup(self._tmp.cleanup)
|
|
471
|
+
self.root = Path(self._tmp.name)
|
|
472
|
+
self.usage_file = self.root / "usage-limits.json"
|
|
473
|
+
self.accounts_dir = self.root / "usage-limits"
|
|
474
|
+
for name, value in (("USAGE_FILE", self.usage_file), ("ACCOUNTS_DIR", self.accounts_dir)):
|
|
475
|
+
patcher = mock.patch.object(ccusage, name, value)
|
|
476
|
+
patcher.start()
|
|
477
|
+
self.addCleanup(patcher.stop)
|
|
478
|
+
self.config_dir = self.root / "config"
|
|
479
|
+
self.config_dir.mkdir()
|
|
480
|
+
env = mock.patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(self.config_dir)}, clear=False)
|
|
481
|
+
env.start()
|
|
482
|
+
self.addCleanup(env.stop)
|
|
483
|
+
|
|
484
|
+
def _login(self, uuid: str, email: str):
|
|
485
|
+
(self.config_dir / ".claude.json").write_text(json.dumps({
|
|
486
|
+
"oauthAccount": {"accountUuid": uuid, "emailAddress": email, "organizationName": "x"},
|
|
487
|
+
}))
|
|
488
|
+
|
|
489
|
+
def test_current_account_reads_uuid_and_email_from_config_dir(self):
|
|
490
|
+
self._login("uuid-a", "a@example.com")
|
|
491
|
+
self.assertEqual(ccusage.current_account(), {"uuid": "uuid-a", "email": "a@example.com"})
|
|
492
|
+
|
|
493
|
+
def test_current_account_is_none_without_login(self):
|
|
494
|
+
self.assertIsNone(ccusage.current_account())
|
|
495
|
+
(self.config_dir / ".claude.json").write_text(json.dumps({"oauthAccount": None}))
|
|
496
|
+
self.assertIsNone(ccusage.current_account())
|
|
497
|
+
|
|
498
|
+
def test_write_keeps_a_snapshot_per_account(self):
|
|
499
|
+
self._login("uuid-a", "a@example.com")
|
|
500
|
+
account_a = ccusage.current_account()
|
|
501
|
+
ccusage.write_usage_file({"7d": {"pct": 91}}, account_a)
|
|
502
|
+
self._login("uuid-b", "b@example.com")
|
|
503
|
+
account_b = ccusage.current_account()
|
|
504
|
+
ccusage.write_usage_file({"7d": {"pct": 5}}, account_b)
|
|
505
|
+
|
|
506
|
+
snapshot_a = json.loads((self.accounts_dir / "uuid-a.json").read_text())
|
|
507
|
+
snapshot_b = json.loads((self.accounts_dir / "uuid-b.json").read_text())
|
|
508
|
+
main = json.loads(self.usage_file.read_text())
|
|
509
|
+
self.assertEqual(snapshot_a["7d"]["pct"], 91)
|
|
510
|
+
self.assertEqual(snapshot_a["account"]["email"], "a@example.com")
|
|
511
|
+
self.assertEqual(snapshot_b["7d"]["pct"], 5)
|
|
512
|
+
self.assertEqual(main, snapshot_b)
|
|
513
|
+
|
|
514
|
+
def test_snapshot_skipped_when_login_changes_during_fetch(self):
|
|
515
|
+
self._login("uuid-a", "a@example.com")
|
|
516
|
+
account_before_fetch = ccusage.current_account()
|
|
517
|
+
self._login("uuid-b", "b@example.com")
|
|
518
|
+
ccusage.write_usage_file({"7d": {"pct": 40}}, account_before_fetch)
|
|
519
|
+
|
|
520
|
+
self.assertFalse(self.accounts_dir.exists())
|
|
521
|
+
main = json.loads(self.usage_file.read_text())
|
|
522
|
+
self.assertNotIn("account", main)
|
|
523
|
+
self.assertEqual(main["7d"]["pct"], 40)
|
|
524
|
+
|
|
525
|
+
def test_refresh_writes_snapshot_for_logged_in_account(self):
|
|
526
|
+
self._login("uuid-a", "a@example.com")
|
|
527
|
+
api = {"limits": [{"name": "seven_day", "utilization": 12.0, "resets_at": None}]}
|
|
528
|
+
with (
|
|
529
|
+
mock.patch.object(ccusage, "fetch_usage", return_value=api),
|
|
530
|
+
mock.patch.object(ccusage, "get_plan", return_value="max_20x"),
|
|
531
|
+
mock.patch("sys.stdout", new_callable=io.StringIO),
|
|
532
|
+
):
|
|
533
|
+
ccusage.cmd_refresh()
|
|
534
|
+
|
|
535
|
+
snapshot = json.loads((self.accounts_dir / "uuid-a.json").read_text())
|
|
536
|
+
self.assertEqual(snapshot["account"]["uuid"], "uuid-a")
|
|
537
|
+
self.assertEqual(snapshot["plan"], "max_20x")
|
|
538
|
+
|
|
539
|
+
|
|
467
540
|
if __name__ == "__main__":
|
|
468
541
|
unittest.main()
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|