pocketshell 0.4.45__tar.gz → 0.4.46__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.
- {pocketshell-0.4.45 → pocketshell-0.4.46}/PKG-INFO +2 -2
- {pocketshell-0.4.45 → pocketshell-0.4.46}/pyproject.toml +18 -14
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/engines.py +6 -1
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/usage.py +46 -85
- pocketshell-0.4.46/tests/data/quse-0.0.15-usage.json +226 -0
- pocketshell-0.4.46/tests/data/quse-0.0.15-usage.ndjson +6 -0
- pocketshell-0.4.46/tests/data/quse-0.0.15-usage.provenance.txt +32 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_daemon.py +30 -18
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_engines.py +32 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_usage.py +321 -139
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_usage_capture.py +26 -22
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_usage_reset.py +1 -1
- {pocketshell-0.4.45 → pocketshell-0.4.46}/uv.lock +6 -6
- pocketshell-0.4.45/tests/data/quse-0.0.14-usage.json +0 -184
- pocketshell-0.4.45/tests/data/quse-0.0.14-usage.provenance.txt +0 -18
- {pocketshell-0.4.45 → pocketshell-0.4.46}/.gitignore +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/README.md +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/scheduler/README.md +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/scripts/sync-readme-exit-codes.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/agent_card_push.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/agent_log.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/agents.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/aplexer.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/cards.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/cli.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/daemon.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/hooks.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/send.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/serve.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/session_enum.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/sessions.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/__init__.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/conftest.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/data/quse-0.0.11-usage.json +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/data/quse-0.0.13-usage.json +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/data/quse-0.0.9-usage.json +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_agent_card_push.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_agent_log.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_agents.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_agents_kind_target.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_agents_source_target.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_cards.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_cards_push_notify.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_cli.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_daemon_fallback.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_env.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_github.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_hooks.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_logs.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_push.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_repos.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_resume.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_send.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_serve.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_session_enum.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_sessions.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_ssh_fixture_penalties.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_tree.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_watch_ci.py +0 -0
- {pocketshell-0.4.45 → pocketshell-0.4.46}/tests/test_watch_ci_cancelled.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.46
|
|
4
4
|
Summary: Unified server-side Python utility for the PocketShell Android client.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
|
|
6
6
|
Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.11
|
|
|
21
21
|
Requires-Dist: click>=8.2.0
|
|
22
22
|
Requires-Dist: google-auth>=2.0.0
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
|
-
Requires-Dist: quse==0.0.
|
|
24
|
+
Requires-Dist: quse==0.0.15
|
|
25
25
|
Requires-Dist: tmuxctl>=0.3.5
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: pytest>=8.4.0; extra == 'dev'
|
|
@@ -14,7 +14,7 @@ name = "pocketshell"
|
|
|
14
14
|
# scripts/check-pypi-version.sh enforces the stamped value matches the tag;
|
|
15
15
|
# scripts/check-version-coupling.sh enforces the derivation itself is
|
|
16
16
|
# single-sourced. See tools/pocketshell/README.md ("Version coupling").
|
|
17
|
-
version = "0.4.
|
|
17
|
+
version = "0.4.46"
|
|
18
18
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
requires-python = ">=3.11"
|
|
@@ -43,16 +43,19 @@ dependencies = [
|
|
|
43
43
|
# Usage backend + single source of truth for the unified provider schema
|
|
44
44
|
# (issue #1318). Pinned exactly: `pocketshell usage --json` expects quse's
|
|
45
45
|
# provider-keyed `--json` document and fails loudly on schema drift, so the
|
|
46
|
-
# version is frozen, not a range. The published 0.0.
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
|
|
46
|
+
# version is frozen, not a range. The published 0.0.15 wheel (upstream
|
|
47
|
+
# a86959e, issue #2293) is the six-provider CANONICAL producer: every
|
|
48
|
+
# record already carries the top-level `windows` map the Android parser
|
|
49
|
+
# reads, so the host boundary only injects the provider key and forwards
|
|
50
|
+
# the record unchanged. #2283's `short_term` / `long_term` translation for
|
|
51
|
+
# the older wheel is hard-cut (D22) — a legacy-shaped record now fails
|
|
52
|
+
# loudly. 0.0.15 is also the first published wheel that reports the `go`
|
|
53
|
+
# provider (OpenCode on the Go backend), so the OpenCode Go quota finally
|
|
54
|
+
# reaches the Usage panel; it is a provider of the pinned wheel, not a
|
|
55
|
+
# separate producer contract. 0.0.13 (#2195) historically added the `grok`
|
|
56
|
+
# provider; 0.0.11 (#1564) labeled each Codex window from its actual
|
|
57
|
+
# `limit_window_seconds`.
|
|
58
|
+
"quse==0.0.15",
|
|
56
59
|
# FCM HTTP v1 push delivery (#690): service-account OAuth2 bearer minting
|
|
57
60
|
# for `pocketshell push` / the `usage --capture` reset-push send. Imported
|
|
58
61
|
# lazily and fail-soft — a host without it (or without a Firebase
|
|
@@ -128,9 +131,10 @@ exclude = [
|
|
|
128
131
|
[tool.uv]
|
|
129
132
|
# Keep the lockfile's reproducibility cutoff project-local. The host may have
|
|
130
133
|
# a rolling global `exclude-newer` policy that predates deliberately pinned
|
|
131
|
-
# releases (quse 0.0.
|
|
132
|
-
# `uv.lock` options cutoff and must be used when checking this lock.
|
|
133
|
-
|
|
134
|
+
# releases (quse 0.0.15 and tmuxctl 0.3.5); this exact value is also the
|
|
135
|
+
# `uv.lock` options cutoff and must be used when checking this lock. Moved
|
|
136
|
+
# past quse 0.0.15's 2026-08-28T12:09:07Z PyPI upload for issue #2293.
|
|
137
|
+
exclude-newer = "2026-08-28T22:00:00Z"
|
|
134
138
|
|
|
135
139
|
[tool.pytest.ini_options]
|
|
136
140
|
testpaths = ["tests"]
|
|
@@ -280,7 +280,12 @@ def builtin_manifests() -> tuple[EngineManifest, ...]:
|
|
|
280
280
|
harness="opencode",
|
|
281
281
|
label="OpenCode",
|
|
282
282
|
provider_mark="OpenCode",
|
|
283
|
-
usage_provider
|
|
283
|
+
# Issue #2293: `usage_provider` names the quse provider whose quota
|
|
284
|
+
# belongs to this engine, and quse has no provider called
|
|
285
|
+
# "opencode" — the OpenCode-on-Go backend is reported as `go`
|
|
286
|
+
# (quse.opencode_go_quota). The old value would have made
|
|
287
|
+
# `pocketshell usage opencode` fail with "Unknown provider".
|
|
288
|
+
usage_provider="go",
|
|
284
289
|
launch=LaunchSpec(argv=("opencode",)),
|
|
285
290
|
),
|
|
286
291
|
EngineManifest(
|
|
@@ -4,17 +4,25 @@ Implementation delegates to the **pinned** `quse` CLI via `subprocess.run`
|
|
|
4
4
|
and normalizes its provider-keyed `--json` document into the per-provider
|
|
5
5
|
NDJSON the Android app consumes. Human output is proxied verbatim.
|
|
6
6
|
|
|
7
|
-
The pinned PyPI `quse==0.0.
|
|
8
|
-
object whose records carry
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
The pinned PyPI `quse==0.0.15` contract (upstream a86959e, issue #2293) is a
|
|
8
|
+
six-provider, provider-keyed object whose records ALREADY carry the canonical
|
|
9
|
+
top-level `windows` map the Android parser reads — `claude`, `codex`,
|
|
10
|
+
`copilot`, `go` (OpenCode on the Go backend), `grok`, `zai`. PocketShell's
|
|
11
|
+
producer boundary is therefore a passthrough: it injects the provider name
|
|
12
|
+
from the object key and forwards the record unchanged. It never re-derives
|
|
13
|
+
quota values, reset times, window labels, or provider details.
|
|
14
|
+
|
|
15
|
+
#2283's `short_term` / `long_term` translation existed only because the
|
|
16
|
+
published wheel lagged quse HEAD; with the pin at 0.0.15 that producer no
|
|
17
|
+
longer exists, so the translation is hard-cut (D22) and a legacy-shaped record
|
|
18
|
+
now fails loudly. A schema mismatch (non-JSON / non-object payload, malformed
|
|
19
|
+
window container, or a missing canonical `windows` map) raises rather than
|
|
20
|
+
silently emptying the panel.
|
|
21
|
+
|
|
22
|
+
`pocketshell usage` keeps NO provider allowlist of its own: the positional
|
|
23
|
+
`provider` argument is forwarded verbatim to the pinned quse, which owns
|
|
24
|
+
provider validation and its error message. That is why "Unknown provider 'go'"
|
|
25
|
+
was only ever fixable by bumping the pin.
|
|
18
26
|
|
|
19
27
|
quse is a hard dependency of pocketshell (see `pyproject.toml`), so its
|
|
20
28
|
console-script ships in the SAME bin directory as the running interpreter.
|
|
@@ -184,90 +192,43 @@ def _canonicalize_quse_record(
|
|
|
184
192
|
) -> dict[str, Any]:
|
|
185
193
|
"""Normalize one provider record at the PocketShell producer boundary.
|
|
186
194
|
|
|
187
|
-
The pinned
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
The pinned ``quse==0.0.15`` wheel IS the canonical producer: every record
|
|
196
|
+
already carries the top-level ``windows`` map the Android parser reads. The
|
|
197
|
+
only thing this boundary does is inject the ``provider`` name that quse
|
|
198
|
+
keeps in the object key — quse's ``--json`` document is provider-keyed and
|
|
199
|
+
the app wire is one self-describing record per line. Nothing else is
|
|
200
|
+
touched: windows, percentages, reset times, ``rolling`` flags and
|
|
201
|
+
provider-owned ``details`` are forwarded verbatim.
|
|
202
|
+
|
|
203
|
+
Fail loud (D22 hard cut): a record without a canonical ``windows`` object
|
|
204
|
+
is a schema drift — most plausibly a stale/shadowed quse — and must surface
|
|
205
|
+
as an error, not be silently re-shaped. There is deliberately no
|
|
206
|
+
``short_term`` / ``long_term`` fallback here any more (#2283 → #2293).
|
|
192
207
|
"""
|
|
193
|
-
if "windows" in record:
|
|
194
|
-
windows = record["windows"]
|
|
195
|
-
if not isinstance(windows, dict):
|
|
196
|
-
raise ValueError(
|
|
197
|
-
f"quse provider '{provider}' top-level 'windows' is not a JSON object"
|
|
198
|
-
)
|
|
199
|
-
if "short_term" in record or "long_term" in record:
|
|
200
|
-
raise ValueError(
|
|
201
|
-
f"quse provider '{provider}' mixes canonical 'windows' with "
|
|
202
|
-
"legacy short_term/long_term fields"
|
|
203
|
-
)
|
|
204
|
-
return {"provider": provider, **record}
|
|
205
|
-
if "short_term" not in record and "long_term" not in record:
|
|
208
|
+
if "windows" not in record:
|
|
206
209
|
raise ValueError(
|
|
207
|
-
f"quse provider '{provider}' is missing canonical 'windows'
|
|
208
|
-
"legacy window fields 'short_term' and 'long_term'"
|
|
210
|
+
f"quse provider '{provider}' is missing the canonical 'windows' map"
|
|
209
211
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
source_window = record[source_key]
|
|
216
|
-
if not isinstance(source_window, dict):
|
|
217
|
-
raise ValueError(
|
|
218
|
-
f"quse provider '{provider}' field '{source_key}' is not a JSON object"
|
|
219
|
-
)
|
|
220
|
-
if "percent_remaining" not in source_window:
|
|
221
|
-
raise ValueError(
|
|
222
|
-
f"quse provider '{provider}' field '{source_key}' is missing "
|
|
223
|
-
"percent_remaining"
|
|
224
|
-
)
|
|
225
|
-
if "reset_at" not in source_window:
|
|
226
|
-
raise ValueError(
|
|
227
|
-
f"quse provider '{provider}' field '{source_key}' is missing reset_at"
|
|
228
|
-
)
|
|
229
|
-
# Legacy quse represents a span that does not apply to a provider with
|
|
230
|
-
# a null percentage. Omit it at this compatibility boundary; canonical
|
|
231
|
-
# producers may retain that source span and the Android parser omits
|
|
232
|
-
# its non-renderable row.
|
|
233
|
-
if source_window["percent_remaining"] is None:
|
|
234
|
-
continue
|
|
235
|
-
label = source_window.get("window") or source_key
|
|
236
|
-
if not isinstance(label, str) or not label.strip():
|
|
237
|
-
raise ValueError(
|
|
238
|
-
f"quse provider '{provider}' field '{source_key}' has an invalid window label"
|
|
239
|
-
)
|
|
240
|
-
if label in windows:
|
|
241
|
-
raise ValueError(
|
|
242
|
-
f"quse provider '{provider}' has duplicate window label '{label}'"
|
|
243
|
-
)
|
|
244
|
-
windows[label] = {
|
|
245
|
-
"percent_remaining": source_window["percent_remaining"],
|
|
246
|
-
"reset_at": source_window["reset_at"],
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
flattened: dict[str, Any] = {"provider": provider, **record}
|
|
250
|
-
flattened.pop("short_term", None)
|
|
251
|
-
flattened.pop("long_term", None)
|
|
252
|
-
flattened["windows"] = windows
|
|
253
|
-
return flattened
|
|
212
|
+
if not isinstance(record["windows"], dict):
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"quse provider '{provider}' top-level 'windows' is not a JSON object"
|
|
215
|
+
)
|
|
216
|
+
return {"provider": provider, **record}
|
|
254
217
|
|
|
255
218
|
|
|
256
219
|
def normalize_usage_stdout(stdout: str) -> str:
|
|
257
220
|
"""Flatten quse's provider-keyed object into per-provider NDJSON.
|
|
258
221
|
|
|
259
|
-
Published
|
|
260
|
-
``
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
function never invents percentages or reset times, and Android receives
|
|
264
|
-
only the canonical map.
|
|
222
|
+
Published ``quse==0.0.15`` records already carry the canonical top-level
|
|
223
|
+
``windows`` map, so each record passes through unchanged apart from the
|
|
224
|
+
injected ``provider`` key. This function never invents percentages or
|
|
225
|
+
reset times.
|
|
265
226
|
|
|
266
227
|
Strict / fail-loud: non-JSON stdout, a non-object top-level payload, a
|
|
267
|
-
non-object provider value, or
|
|
268
|
-
``ValueError`` so a schema mismatch fails visibly
|
|
269
|
-
emptying the usage panel. Empty/blank stdout passes
|
|
270
|
-
caller decides what an empty read means).
|
|
228
|
+
non-object provider value, or a record missing/mis-typing the canonical
|
|
229
|
+
``windows`` map all raise ``ValueError`` so a schema mismatch fails visibly
|
|
230
|
+
instead of silently emptying the usage panel. Empty/blank stdout passes
|
|
231
|
+
through untouched (the caller decides what an empty read means).
|
|
271
232
|
"""
|
|
272
233
|
if not stdout.strip():
|
|
273
234
|
return stdout
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
{
|
|
2
|
+
"claude": {
|
|
3
|
+
"details": {
|
|
4
|
+
"limit_reached": false,
|
|
5
|
+
"subscription": null,
|
|
6
|
+
"windows": {
|
|
7
|
+
"five_hour": {
|
|
8
|
+
"reset_at": "2026-08-28T16:20:00Z",
|
|
9
|
+
"used_percent": 6.0
|
|
10
|
+
},
|
|
11
|
+
"seven_day": {
|
|
12
|
+
"reset_at": "2026-09-03T15:00:00Z",
|
|
13
|
+
"used_percent": 10.0
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"error": null,
|
|
18
|
+
"status": "ok",
|
|
19
|
+
"windows": {
|
|
20
|
+
"5h": {
|
|
21
|
+
"percent_remaining": 94.0,
|
|
22
|
+
"reset_at": "2026-08-28T16:20:00Z",
|
|
23
|
+
"rolling": false
|
|
24
|
+
},
|
|
25
|
+
"7d": {
|
|
26
|
+
"percent_remaining": 90.0,
|
|
27
|
+
"reset_at": "2026-09-03T15:00:00Z"
|
|
28
|
+
},
|
|
29
|
+
"monthly": {
|
|
30
|
+
"percent_remaining": null,
|
|
31
|
+
"reset_at": null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"codex": {
|
|
36
|
+
"details": {
|
|
37
|
+
"limit_reached": false,
|
|
38
|
+
"reset_credits": [
|
|
39
|
+
{
|
|
40
|
+
"expires_at": "2026-09-21T00:13:17Z",
|
|
41
|
+
"status": "available",
|
|
42
|
+
"title": "Full reset"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"reset_credits_available": 1,
|
|
46
|
+
"reset_credits_error": null,
|
|
47
|
+
"windows": {
|
|
48
|
+
"primary_window": {
|
|
49
|
+
"limit_window_seconds": 604800,
|
|
50
|
+
"present": true,
|
|
51
|
+
"reset_at": "2026-09-03T16:26:48Z",
|
|
52
|
+
"used_percent": 13.0
|
|
53
|
+
},
|
|
54
|
+
"secondary_window": {
|
|
55
|
+
"limit_window_seconds": null,
|
|
56
|
+
"present": false,
|
|
57
|
+
"reset_at": null,
|
|
58
|
+
"used_percent": 0.0
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"error": null,
|
|
63
|
+
"status": "ok",
|
|
64
|
+
"windows": {
|
|
65
|
+
"5h": {
|
|
66
|
+
"percent_remaining": null,
|
|
67
|
+
"reset_at": null,
|
|
68
|
+
"rolling": false
|
|
69
|
+
},
|
|
70
|
+
"7d": {
|
|
71
|
+
"percent_remaining": 87.0,
|
|
72
|
+
"reset_at": "2026-09-03T16:26:48Z"
|
|
73
|
+
},
|
|
74
|
+
"monthly": {
|
|
75
|
+
"percent_remaining": null,
|
|
76
|
+
"reset_at": null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"copilot": {
|
|
81
|
+
"details": {
|
|
82
|
+
"limit_reached": true,
|
|
83
|
+
"premium_entitlement": 1500,
|
|
84
|
+
"premium_percent_remaining": 0.0,
|
|
85
|
+
"premium_remaining": -1
|
|
86
|
+
},
|
|
87
|
+
"error": null,
|
|
88
|
+
"status": "ok",
|
|
89
|
+
"windows": {
|
|
90
|
+
"5h": {
|
|
91
|
+
"percent_remaining": null,
|
|
92
|
+
"reset_at": null,
|
|
93
|
+
"rolling": false
|
|
94
|
+
},
|
|
95
|
+
"7d": {
|
|
96
|
+
"percent_remaining": null,
|
|
97
|
+
"reset_at": null
|
|
98
|
+
},
|
|
99
|
+
"monthly": {
|
|
100
|
+
"percent_remaining": 0.0,
|
|
101
|
+
"reset_at": "2026-09-01T00:00:00Z"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"go": {
|
|
106
|
+
"details": {
|
|
107
|
+
"limit_reached": false,
|
|
108
|
+
"max_used_percent": 64.0
|
|
109
|
+
},
|
|
110
|
+
"error": null,
|
|
111
|
+
"status": "ok",
|
|
112
|
+
"windows": {
|
|
113
|
+
"5h": {
|
|
114
|
+
"percent_remaining": 36.0,
|
|
115
|
+
"reset_at": "2026-08-28T13:36:26Z",
|
|
116
|
+
"rolling": true
|
|
117
|
+
},
|
|
118
|
+
"7d": {
|
|
119
|
+
"percent_remaining": 74.0,
|
|
120
|
+
"reset_at": "2026-08-31T00:00:00Z"
|
|
121
|
+
},
|
|
122
|
+
"monthly": {
|
|
123
|
+
"percent_remaining": 86.0,
|
|
124
|
+
"reset_at": "2026-09-22T06:20:28Z"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"grok": {
|
|
129
|
+
"details": {
|
|
130
|
+
"has_grok_code_access": true,
|
|
131
|
+
"is_unified_billing_user": true,
|
|
132
|
+
"limit_reached": true,
|
|
133
|
+
"on_demand_cap": 0.0,
|
|
134
|
+
"on_demand_used": 0.0,
|
|
135
|
+
"prepaid_balance": 0.0,
|
|
136
|
+
"product_usage": [
|
|
137
|
+
{
|
|
138
|
+
"product": "GrokBuild",
|
|
139
|
+
"usage_percent": 100.0
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"resets": [
|
|
143
|
+
{
|
|
144
|
+
"expires_at": "2026-09-12T18:49:00Z",
|
|
145
|
+
"token_id": "restok_vpYDqo"
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"resets_available": 1,
|
|
149
|
+
"resets_error": null,
|
|
150
|
+
"windows": {
|
|
151
|
+
"monthly": {
|
|
152
|
+
"limit": null,
|
|
153
|
+
"present": false,
|
|
154
|
+
"reset_at": null,
|
|
155
|
+
"used": null,
|
|
156
|
+
"used_percent": null
|
|
157
|
+
},
|
|
158
|
+
"weekly": {
|
|
159
|
+
"limit": 0.0,
|
|
160
|
+
"present": true,
|
|
161
|
+
"reset_at": "2026-09-01T00:08:17Z",
|
|
162
|
+
"used": 0.0,
|
|
163
|
+
"used_percent": 100.0
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"error": null,
|
|
168
|
+
"status": "ok",
|
|
169
|
+
"windows": {
|
|
170
|
+
"5h": {
|
|
171
|
+
"percent_remaining": null,
|
|
172
|
+
"reset_at": null,
|
|
173
|
+
"rolling": false
|
|
174
|
+
},
|
|
175
|
+
"7d": {
|
|
176
|
+
"percent_remaining": 0.0,
|
|
177
|
+
"reset_at": "2026-09-01T00:08:17Z"
|
|
178
|
+
},
|
|
179
|
+
"monthly": {
|
|
180
|
+
"percent_remaining": null,
|
|
181
|
+
"reset_at": null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"zai": {
|
|
186
|
+
"details": {
|
|
187
|
+
"limit_reached": false,
|
|
188
|
+
"max_used_percent": 45.0,
|
|
189
|
+
"windows": {
|
|
190
|
+
"five_hour": {
|
|
191
|
+
"limit": null,
|
|
192
|
+
"present": true,
|
|
193
|
+
"remaining": null,
|
|
194
|
+
"reset_at": null,
|
|
195
|
+
"used_percent": 1.0,
|
|
196
|
+
"window_hours": 5
|
|
197
|
+
},
|
|
198
|
+
"weekly": {
|
|
199
|
+
"limit": null,
|
|
200
|
+
"present": true,
|
|
201
|
+
"remaining": null,
|
|
202
|
+
"reset_at": "2026-09-03T14:04:58Z",
|
|
203
|
+
"used_percent": 45.0,
|
|
204
|
+
"window_hours": null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"error": null,
|
|
209
|
+
"status": "ok",
|
|
210
|
+
"windows": {
|
|
211
|
+
"5h": {
|
|
212
|
+
"percent_remaining": 99.0,
|
|
213
|
+
"reset_at": null,
|
|
214
|
+
"rolling": true
|
|
215
|
+
},
|
|
216
|
+
"7d": {
|
|
217
|
+
"percent_remaining": 55.0,
|
|
218
|
+
"reset_at": "2026-09-03T14:04:58Z"
|
|
219
|
+
},
|
|
220
|
+
"monthly": {
|
|
221
|
+
"percent_remaining": null,
|
|
222
|
+
"reset_at": null
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"details": {"limit_reached": false, "subscription": null, "windows": {"five_hour": {"reset_at": "2026-08-28T16:20:00Z", "used_percent": 6.0}, "seven_day": {"reset_at": "2026-09-03T15:00:00Z", "used_percent": 10.0}}}, "error": null, "provider": "claude", "status": "ok", "windows": {"5h": {"percent_remaining": 94.0, "reset_at": "2026-08-28T16:20:00Z", "rolling": false}, "7d": {"percent_remaining": 90.0, "reset_at": "2026-09-03T15:00:00Z"}, "monthly": {"percent_remaining": null, "reset_at": null}}}
|
|
2
|
+
{"details": {"limit_reached": false, "reset_credits": [{"expires_at": "2026-09-21T00:13:17Z", "status": "available", "title": "Full reset"}], "reset_credits_available": 1, "reset_credits_error": null, "windows": {"primary_window": {"limit_window_seconds": 604800, "present": true, "reset_at": "2026-09-03T16:26:48Z", "used_percent": 13.0}, "secondary_window": {"limit_window_seconds": null, "present": false, "reset_at": null, "used_percent": 0.0}}}, "error": null, "provider": "codex", "status": "ok", "windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, "7d": {"percent_remaining": 87.0, "reset_at": "2026-09-03T16:26:48Z"}, "monthly": {"percent_remaining": null, "reset_at": null}}}
|
|
3
|
+
{"details": {"limit_reached": true, "premium_entitlement": 1500, "premium_percent_remaining": 0.0, "premium_remaining": -1}, "error": null, "provider": "copilot", "status": "ok", "windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, "7d": {"percent_remaining": null, "reset_at": null}, "monthly": {"percent_remaining": 0.0, "reset_at": "2026-09-01T00:00:00Z"}}}
|
|
4
|
+
{"details": {"limit_reached": false, "max_used_percent": 64.0}, "error": null, "provider": "go", "status": "ok", "windows": {"5h": {"percent_remaining": 36.0, "reset_at": "2026-08-28T13:36:26Z", "rolling": true}, "7d": {"percent_remaining": 74.0, "reset_at": "2026-08-31T00:00:00Z"}, "monthly": {"percent_remaining": 86.0, "reset_at": "2026-09-22T06:20:28Z"}}}
|
|
5
|
+
{"details": {"has_grok_code_access": true, "is_unified_billing_user": true, "limit_reached": true, "on_demand_cap": 0.0, "on_demand_used": 0.0, "prepaid_balance": 0.0, "product_usage": [{"product": "GrokBuild", "usage_percent": 100.0}], "resets": [{"expires_at": "2026-09-12T18:49:00Z", "token_id": "restok_vpYDqo"}], "resets_available": 1, "resets_error": null, "windows": {"monthly": {"limit": null, "present": false, "reset_at": null, "used": null, "used_percent": null}, "weekly": {"limit": 0.0, "present": true, "reset_at": "2026-09-01T00:08:17Z", "used": 0.0, "used_percent": 100.0}}}, "error": null, "provider": "grok", "status": "ok", "windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, "7d": {"percent_remaining": 0.0, "reset_at": "2026-09-01T00:08:17Z"}, "monthly": {"percent_remaining": null, "reset_at": null}}}
|
|
6
|
+
{"details": {"limit_reached": false, "max_used_percent": 45.0, "windows": {"five_hour": {"limit": null, "present": true, "remaining": null, "reset_at": null, "used_percent": 1.0, "window_hours": 5}, "weekly": {"limit": null, "present": true, "remaining": null, "reset_at": "2026-09-03T14:04:58Z", "used_percent": 45.0, "window_hours": null}}}, "error": null, "provider": "zai", "status": "ok", "windows": {"5h": {"percent_remaining": 99.0, "reset_at": null, "rolling": true}, "7d": {"percent_remaining": 55.0, "reset_at": "2026-09-03T14:04:58Z"}, "monthly": {"percent_remaining": null, "reset_at": null}}}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Fixture provenance for quse==0.0.15 (issue #2293)
|
|
2
|
+
|
|
3
|
+
Source package: the PyPI wheel quse-0.0.15-py3-none-any.whl
|
|
4
|
+
PyPI upload: 2026-08-28T12:09:07Z
|
|
5
|
+
Wheel SHA-256: a9138ed2e4f12a01812103332fe27b94de9f96a2413212c59558052481815793
|
|
6
|
+
Upstream commit: a86959edeef1c78a2a0084d3488fca487135b1b0
|
|
7
|
+
("Add unified Go and quota output" — the unified `windows` map + the `go`
|
|
8
|
+
provider). 0.0.14 predates it; see issue #2274 / #2283 for the interim
|
|
9
|
+
translate-at-the-boundary design this release retires.
|
|
10
|
+
|
|
11
|
+
Capture command (using the published wheel, not a checkout):
|
|
12
|
+
PYTHONPATH=<unpacked quse-0.0.15 wheel> <python> -c \
|
|
13
|
+
'from quse.cli import main; raise SystemExit(main(["--json"]))'
|
|
14
|
+
Captured: 2026-08-28 on the maintainer's dev box (real live provider data).
|
|
15
|
+
|
|
16
|
+
Observed published contract:
|
|
17
|
+
providers: claude, codex, copilot, go, grok, zai
|
|
18
|
+
record fields: details, error, status, windows
|
|
19
|
+
every record carries the canonical top-level `windows` map keyed by
|
|
20
|
+
quse.usage.CANONICAL_WINDOWS = ("5h", "7d", "monthly"); the `5h` entry also
|
|
21
|
+
carries `rolling: bool`. A span that does not apply to a provider is present
|
|
22
|
+
with `percent_remaining: null` rather than omitted.
|
|
23
|
+
no `short_term` / `long_term` fields on any record
|
|
24
|
+
|
|
25
|
+
Because 0.0.15 IS the canonical producer, `pocketshell usage --json` performs no
|
|
26
|
+
schema translation any more: `_canonicalize_quse_record` injects the provider
|
|
27
|
+
key and forwards the record verbatim. The sibling file
|
|
28
|
+
`quse-0.0.15-usage.ndjson` is the EXACT output of
|
|
29
|
+
`normalize_usage_stdout(quse-0.0.15-usage.json)` and is byte-compared against
|
|
30
|
+
the producer in tests/test_usage.py, and mirrored into
|
|
31
|
+
shared/core-usage/src/test/resources/ so the Android parser test consumes the
|
|
32
|
+
real producer bytes rather than a Kotlin re-implementation.
|
|
@@ -285,9 +285,12 @@ def test_usage_fetch_round_trip(
|
|
|
285
285
|
provider-keyed output into per-provider NDJSON (issue #1318)."""
|
|
286
286
|
fake_bin = tmp_path / "bin"
|
|
287
287
|
python_exe = _fake_python_bin(fake_bin)
|
|
288
|
-
# Published quse 0.0.
|
|
289
|
-
# daemon
|
|
290
|
-
payload =
|
|
288
|
+
# Published quse 0.0.15 emits a provider-keyed CANONICAL object; the
|
|
289
|
+
# daemon injects the provider key and forwards the record unchanged.
|
|
290
|
+
payload = (
|
|
291
|
+
'{"codex": {"status": "ok", '
|
|
292
|
+
'"windows": {"5h": {"percent_remaining": 77.0, "reset_at": null, "rolling": false}}}}\n'
|
|
293
|
+
)
|
|
291
294
|
_write_fake_quse(fake_bin, payload=payload)
|
|
292
295
|
|
|
293
296
|
proc = _spawn_daemon(sandbox_socket, python_exe=python_exe)
|
|
@@ -323,12 +326,15 @@ def test_two_concurrent_clients_cache_hit_is_faster(
|
|
|
323
326
|
python_exe = _fake_python_bin(fake_bin)
|
|
324
327
|
payload = (
|
|
325
328
|
'{"codex": {"status": "ok", '
|
|
326
|
-
'"
|
|
327
|
-
'"
|
|
329
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
330
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
331
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\n'
|
|
328
332
|
)
|
|
329
333
|
# The daemon flattens the provider-keyed payload into NDJSON before
|
|
330
334
|
# caching; the cold and warm responses both carry this flattened form.
|
|
331
|
-
flattened =
|
|
335
|
+
flattened = (
|
|
336
|
+
'{"provider": "codex", "status": "ok", "windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, "7d": {"percent_remaining": null, "reset_at": null}, "monthly": {"percent_remaining": null, "reset_at": null}}}\n'
|
|
337
|
+
)
|
|
332
338
|
_write_fake_quse(fake_bin, payload=payload, sleep_secs=0.3)
|
|
333
339
|
|
|
334
340
|
proc = _spawn_daemon(sandbox_socket, python_exe=python_exe)
|
|
@@ -400,8 +406,9 @@ def test_no_cache_param_bypasses_cache(
|
|
|
400
406
|
"n=$((n+1))\n"
|
|
401
407
|
f'echo "$n" > {counter_file}\n'
|
|
402
408
|
'printf \'{"x": {"status": "ok", "call": %s, '
|
|
403
|
-
'"
|
|
404
|
-
'"
|
|
409
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
410
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
411
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\\n\' "$n"\n'
|
|
405
412
|
)
|
|
406
413
|
script.chmod(0o755)
|
|
407
414
|
|
|
@@ -519,13 +526,15 @@ def test_no_daemon_flag_skips_daemon_probe(
|
|
|
519
526
|
# path answered (issue #1318: provider-keyed payloads).
|
|
520
527
|
daemon_payload = (
|
|
521
528
|
'{"fromdaemon": {"status": "ok", '
|
|
522
|
-
'"
|
|
523
|
-
'"
|
|
529
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
530
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
531
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\n'
|
|
524
532
|
)
|
|
525
533
|
subprocess_payload = (
|
|
526
534
|
'{"fromsubprocess": {"status": "ok", '
|
|
527
|
-
'"
|
|
528
|
-
'"
|
|
535
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
536
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
537
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\n'
|
|
529
538
|
)
|
|
530
539
|
_write_fake_quse(fake_bin, payload=subprocess_payload)
|
|
531
540
|
|
|
@@ -569,13 +578,15 @@ def test_usage_uses_daemon_by_default(
|
|
|
569
578
|
fake_bin.mkdir()
|
|
570
579
|
daemon_payload = (
|
|
571
580
|
'{"fromdaemon": {"status": "ok", '
|
|
572
|
-
'"
|
|
573
|
-
'"
|
|
581
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
582
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
583
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\n'
|
|
574
584
|
)
|
|
575
585
|
subprocess_payload = (
|
|
576
586
|
'{"fromsubprocess": {"status": "ok", '
|
|
577
|
-
'"
|
|
578
|
-
'"
|
|
587
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
588
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
589
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\n'
|
|
579
590
|
)
|
|
580
591
|
_write_fake_quse(fake_bin, payload=subprocess_payload)
|
|
581
592
|
|
|
@@ -612,8 +623,9 @@ def test_usage_falls_through_when_daemon_absent(
|
|
|
612
623
|
fake_bin.mkdir()
|
|
613
624
|
payload = (
|
|
614
625
|
'{"fromsubprocess": {"status": "ok", '
|
|
615
|
-
'"
|
|
616
|
-
'"
|
|
626
|
+
'"windows": {"5h": {"percent_remaining": null, "reset_at": null, "rolling": false}, '
|
|
627
|
+
'"7d": {"percent_remaining": null, "reset_at": null}, '
|
|
628
|
+
'"monthly": {"percent_remaining": null, "reset_at": null}}}}\n'
|
|
617
629
|
)
|
|
618
630
|
_write_fake_quse(fake_bin, payload=payload)
|
|
619
631
|
monkeypatch.setattr(
|