pocketshell 0.1.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.
- pocketshell-0.1.0/.gitignore +11 -0
- pocketshell-0.1.0/PKG-INFO +108 -0
- pocketshell-0.1.0/README.md +82 -0
- pocketshell-0.1.0/pyproject.toml +74 -0
- pocketshell-0.1.0/src/pocketshell/__init__.py +14 -0
- pocketshell-0.1.0/src/pocketshell/__main__.py +14 -0
- pocketshell-0.1.0/src/pocketshell/cli.py +67 -0
- pocketshell-0.1.0/src/pocketshell/usage.py +115 -0
- pocketshell-0.1.0/tests/__init__.py +0 -0
- pocketshell-0.1.0/tests/test_usage.py +168 -0
- pocketshell-0.1.0/uv.lock +139 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pocketshell
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified server-side Python utility for the PocketShell Android client.
|
|
5
|
+
Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
|
|
6
|
+
Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
|
|
7
|
+
Author: Alexey Grigorev
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agents,pocketshell,ssh,tmux,usage
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: click>=8.2.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.4.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.15.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# pocketshell-cli
|
|
28
|
+
|
|
29
|
+
Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
|
|
30
|
+
Android client. Replaces the separately-installed `quse` and `tmuxctl`
|
|
31
|
+
utilities the app currently probes for on every remote host.
|
|
32
|
+
|
|
33
|
+
This first release ships the **skeleton plus the `pocketshell usage`
|
|
34
|
+
subcommand only**. Follow-up rounds will add `jobs`, `agent-log`,
|
|
35
|
+
`sessions`, `repos`, and an optional daemon mode. See
|
|
36
|
+
[issue #170](https://github.com/alexeygrigorev/pocketshell/issues/170) for
|
|
37
|
+
the design spike and phased roll-out plan.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
The recommended path is `uv tool install`, which lands the binary on PATH
|
|
42
|
+
under `~/.local/bin/`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install pocketshell-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For local development from a clone:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd tools/pocketshell-cli
|
|
52
|
+
uv venv
|
|
53
|
+
uv pip install -e .
|
|
54
|
+
pocketshell --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`pipx install pocketshell-cli` works the same way for users who prefer
|
|
58
|
+
pipx. Both install paths produce a `pocketshell` binary that the
|
|
59
|
+
PocketShell app's bootstrap probe detects.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
pocketshell usage # human-readable lines, one per provider
|
|
65
|
+
pocketshell usage --json # machine-readable JSON (consumed by the app)
|
|
66
|
+
pocketshell usage codex # filter to a single provider
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The output shape is byte-identical to `quse [provider] [--json]` so any
|
|
70
|
+
consumer that already parses `quse` output keeps working when the app
|
|
71
|
+
routes through `pocketshell usage` instead. Under the hood the first
|
|
72
|
+
release delegates to the `quse` CLI via subprocess; later rounds will
|
|
73
|
+
fold the provider-detection logic in directly and drop the subprocess
|
|
74
|
+
hop.
|
|
75
|
+
|
|
76
|
+
If `quse` is not installed, `pocketshell usage` exits with code 127 and
|
|
77
|
+
prints an install hint to stderr.
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cd tools/pocketshell-cli
|
|
83
|
+
uv venv
|
|
84
|
+
uv pip install -e ".[dev]"
|
|
85
|
+
uv run pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or via the dependency-group:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv sync --group dev
|
|
92
|
+
uv run pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The tests stub `quse.usage.collect_usage` so they run in seconds without
|
|
96
|
+
hitting any provider API.
|
|
97
|
+
|
|
98
|
+
## Why a unified CLI?
|
|
99
|
+
|
|
100
|
+
The PocketShell app previously probed for two binaries (`quse`,
|
|
101
|
+
`tmuxctl`) on every host. That meant two installs to keep up to date,
|
|
102
|
+
two probes to surface failures from, and two PATH-discovery edge cases
|
|
103
|
+
(see [issue #41](https://github.com/alexeygrigorev/pocketshell/issues/41)).
|
|
104
|
+
A single `pocketshell` binary collapses those into one install, one
|
|
105
|
+
probe, one bootstrap row. The app keeps detecting `quse` and `tmuxctl`
|
|
106
|
+
as a parallel path while `pocketshell` ramps up to feature parity; once
|
|
107
|
+
parity is reached, the legacy probes are removed in a hard-cut follow-up
|
|
108
|
+
(no compat shim — see decision D22 in `docs/decisions.md`).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# pocketshell-cli
|
|
2
|
+
|
|
3
|
+
Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
|
|
4
|
+
Android client. Replaces the separately-installed `quse` and `tmuxctl`
|
|
5
|
+
utilities the app currently probes for on every remote host.
|
|
6
|
+
|
|
7
|
+
This first release ships the **skeleton plus the `pocketshell usage`
|
|
8
|
+
subcommand only**. Follow-up rounds will add `jobs`, `agent-log`,
|
|
9
|
+
`sessions`, `repos`, and an optional daemon mode. See
|
|
10
|
+
[issue #170](https://github.com/alexeygrigorev/pocketshell/issues/170) for
|
|
11
|
+
the design spike and phased roll-out plan.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
The recommended path is `uv tool install`, which lands the binary on PATH
|
|
16
|
+
under `~/.local/bin/`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install pocketshell-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For local development from a clone:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd tools/pocketshell-cli
|
|
26
|
+
uv venv
|
|
27
|
+
uv pip install -e .
|
|
28
|
+
pocketshell --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`pipx install pocketshell-cli` works the same way for users who prefer
|
|
32
|
+
pipx. Both install paths produce a `pocketshell` binary that the
|
|
33
|
+
PocketShell app's bootstrap probe detects.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
pocketshell usage # human-readable lines, one per provider
|
|
39
|
+
pocketshell usage --json # machine-readable JSON (consumed by the app)
|
|
40
|
+
pocketshell usage codex # filter to a single provider
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The output shape is byte-identical to `quse [provider] [--json]` so any
|
|
44
|
+
consumer that already parses `quse` output keeps working when the app
|
|
45
|
+
routes through `pocketshell usage` instead. Under the hood the first
|
|
46
|
+
release delegates to the `quse` CLI via subprocess; later rounds will
|
|
47
|
+
fold the provider-detection logic in directly and drop the subprocess
|
|
48
|
+
hop.
|
|
49
|
+
|
|
50
|
+
If `quse` is not installed, `pocketshell usage` exits with code 127 and
|
|
51
|
+
prints an install hint to stderr.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd tools/pocketshell-cli
|
|
57
|
+
uv venv
|
|
58
|
+
uv pip install -e ".[dev]"
|
|
59
|
+
uv run pytest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or via the dependency-group:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv sync --group dev
|
|
66
|
+
uv run pytest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The tests stub `quse.usage.collect_usage` so they run in seconds without
|
|
70
|
+
hitting any provider API.
|
|
71
|
+
|
|
72
|
+
## Why a unified CLI?
|
|
73
|
+
|
|
74
|
+
The PocketShell app previously probed for two binaries (`quse`,
|
|
75
|
+
`tmuxctl`) on every host. That meant two installs to keep up to date,
|
|
76
|
+
two probes to surface failures from, and two PATH-discovery edge cases
|
|
77
|
+
(see [issue #41](https://github.com/alexeygrigorev/pocketshell/issues/41)).
|
|
78
|
+
A single `pocketshell` binary collapses those into one install, one
|
|
79
|
+
probe, one bootstrap row. The app keeps detecting `quse` and `tmuxctl`
|
|
80
|
+
as a parallel path while `pocketshell` ramps up to feature parity; once
|
|
81
|
+
parity is reached, the legacy probes are removed in a hard-cut follow-up
|
|
82
|
+
(no compat shim — see decision D22 in `docs/decisions.md`).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pocketshell"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [{ name = "Alexey Grigorev" }]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
keywords = ["pocketshell", "ssh", "tmux", "agents", "usage"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development",
|
|
24
|
+
"Topic :: System :: Monitoring",
|
|
25
|
+
]
|
|
26
|
+
# First PR delegates `pocketshell usage` to the existing `quse` CLI via
|
|
27
|
+
# subprocess (see `pocketshell/usage.py`) so the new utility does not need
|
|
28
|
+
# `quse` published on PyPI. Later PRs will fold the provider-detection
|
|
29
|
+
# implementation in directly. Pin lower-bound only.
|
|
30
|
+
dependencies = [
|
|
31
|
+
"click>=8.2.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
pocketshell = "pocketshell.cli:main"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/alexeygrigorev/pocketshell"
|
|
39
|
+
Issues = "https://github.com/alexeygrigorev/pocketshell/issues"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.4.0",
|
|
44
|
+
"ruff>=0.15.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
# Mirror of the dev `dependency-groups` so `pip install -e ".[dev]"` and
|
|
49
|
+
# `uv pip install -e ".[dev]"` both work for CI and ad-hoc local setups.
|
|
50
|
+
# `uv sync --group dev` reads `dependency-groups`; PEP-621 tooling reads
|
|
51
|
+
# `optional-dependencies`. Keeping both in lock-step is cheap.
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=8.4.0",
|
|
54
|
+
"ruff>=0.15.0",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/pocketshell"]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.sdist]
|
|
61
|
+
exclude = [
|
|
62
|
+
"/.github",
|
|
63
|
+
"/.pytest_cache",
|
|
64
|
+
"/.ruff_cache",
|
|
65
|
+
"/.venv",
|
|
66
|
+
"/dist",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
line-length = 100
|
|
74
|
+
target-version = "py311"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Unified server-side Python utility for the PocketShell Android client.
|
|
2
|
+
|
|
3
|
+
This package is intended to replace the separately-installed `quse` and
|
|
4
|
+
`tmuxctl` utilities the PocketShell app currently probes for. The first PR
|
|
5
|
+
ships the skeleton plus the `pocketshell usage` subcommand only; later PRs
|
|
6
|
+
will add `jobs`, `agent-log`, `sessions`, `repos`, and daemon mode.
|
|
7
|
+
|
|
8
|
+
See https://github.com/alexeygrigorev/pocketshell/issues/170.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
|
14
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Allow `python -m pocketshell` as an alternative to the installed entry point.
|
|
2
|
+
|
|
3
|
+
Used in CI and developer environments where the console-script shim from
|
|
4
|
+
`pip install -e .` / `uv tool install pocketshell-cli` is not on PATH.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from pocketshell.cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
sys.exit(main())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Top-level Click dispatcher for the unified `pocketshell` CLI.
|
|
2
|
+
|
|
3
|
+
This is the skeleton landed in the first PR of issue
|
|
4
|
+
[#170](https://github.com/alexeygrigorev/pocketshell/issues/170). Only the
|
|
5
|
+
`usage` subcommand is wired up today; later PRs will add `jobs`,
|
|
6
|
+
`agent-log`, `sessions`, `repos`, and `daemon`.
|
|
7
|
+
|
|
8
|
+
Per the D22 locked principle (no backwards compatibility, hard cuts only)
|
|
9
|
+
the eventual goal is for the PocketShell Android app to probe for this
|
|
10
|
+
single binary instead of `quse` / `tmuxctl`. The first PR keeps the
|
|
11
|
+
existing probes in place — parallel detection, not legacy detection — so
|
|
12
|
+
the app keeps working while we ramp up `pocketshell`'s feature parity.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Optional, Sequence
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from pocketshell import __version__
|
|
23
|
+
from pocketshell.usage import usage_command
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group(
|
|
27
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
28
|
+
help=(
|
|
29
|
+
"Unified server-side helper for the PocketShell Android client.\n\n"
|
|
30
|
+
"Subcommands replace the separately-installed `quse` and `tmuxctl` "
|
|
31
|
+
"CLIs. The first PR ships `usage` only; more subcommands will land "
|
|
32
|
+
"in follow-up rounds."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
@click.version_option(__version__, "-V", "--version", prog_name="pocketshell")
|
|
36
|
+
def cli() -> None:
|
|
37
|
+
"""Top-level group. Each subcommand is registered below."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
cli.add_command(usage_command, name="usage")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
44
|
+
"""Entrypoint for both the console-script and `python -m pocketshell`.
|
|
45
|
+
|
|
46
|
+
Returns an integer exit code rather than letting Click call
|
|
47
|
+
`sys.exit` so the function is testable from the unit suite.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
result = cli.main(args=list(argv) if argv is not None else None,
|
|
51
|
+
prog_name="pocketshell",
|
|
52
|
+
standalone_mode=False)
|
|
53
|
+
except click.exceptions.Exit as exc:
|
|
54
|
+
return int(exc.exit_code)
|
|
55
|
+
except click.ClickException as exc:
|
|
56
|
+
exc.show()
|
|
57
|
+
return int(exc.exit_code)
|
|
58
|
+
if result is None:
|
|
59
|
+
return 0
|
|
60
|
+
try:
|
|
61
|
+
return int(result)
|
|
62
|
+
except (TypeError, ValueError):
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
sys.exit(main())
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""`pocketshell usage` subcommand.
|
|
2
|
+
|
|
3
|
+
First-PR implementation: delegate to the existing `quse` CLI via
|
|
4
|
+
`subprocess.run`. The arguments and stdout are proxied through verbatim
|
|
5
|
+
so the JSON payload (and human-readable lines) are byte-identical to
|
|
6
|
+
`quse [provider] [--json]`. The existing Kotlin `QuseUsageJsonParser`
|
|
7
|
+
keeps working when the Android app eventually switches its probe over.
|
|
8
|
+
|
|
9
|
+
Why subprocess instead of `import quse`:
|
|
10
|
+
|
|
11
|
+
- `quse` is currently not published to PyPI; declaring it as a normal
|
|
12
|
+
`pyproject.toml` dependency would break `uv tool install
|
|
13
|
+
pocketshell-cli` for any user (including the maintainer's dev box).
|
|
14
|
+
- Subprocess delegation keeps `pocketshell-cli` decoupled from `quse`'s
|
|
15
|
+
internal module layout, so updates to `quse` don't break the wrapper.
|
|
16
|
+
- The PATH-discovery story for `quse` is already solved on the app side
|
|
17
|
+
(see issue #41 + the `pathOverride` column on `HostEntity`). Wrapping
|
|
18
|
+
`quse` here means the app's existing PATH override mechanism keeps
|
|
19
|
+
working without re-implementation.
|
|
20
|
+
|
|
21
|
+
Later PRs will fold the provider-detection logic in directly so
|
|
22
|
+
`pocketshell-cli` is the canonical implementation and the subprocess
|
|
23
|
+
hop disappears, but that is explicit non-scope here per the brief on
|
|
24
|
+
issue [#170](https://github.com/alexeygrigorev/pocketshell/issues/170).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from typing import Optional, Sequence
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_quse_binary() -> Optional[str]:
|
|
38
|
+
"""Locate the `quse` CLI on PATH, or return ``None`` if absent.
|
|
39
|
+
|
|
40
|
+
Pulled out as a function so the unit suite can monkeypatch it.
|
|
41
|
+
`shutil.which` returns the same path the user would see from
|
|
42
|
+
`command -v quse`, which is the probe the Android app already runs.
|
|
43
|
+
"""
|
|
44
|
+
return shutil.which("quse")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _run_quse(args: Sequence[str]) -> int:
|
|
48
|
+
"""Invoke `quse` with [args]; proxy stdout/stderr and exit code.
|
|
49
|
+
|
|
50
|
+
Using `subprocess.run(..., check=False)` and forwarding the captured
|
|
51
|
+
output rather than `os.execvp` keeps the call testable (the test
|
|
52
|
+
suite can monkeypatch `subprocess.run`) and lets us decorate the
|
|
53
|
+
failure mode with a friendly hint when `quse` is missing.
|
|
54
|
+
"""
|
|
55
|
+
quse_path = _resolve_quse_binary()
|
|
56
|
+
if quse_path is None:
|
|
57
|
+
# Same wording the bootstrap sheet uses so the user sees a
|
|
58
|
+
# consistent message whether they hit the bin via `pocketshell
|
|
59
|
+
# usage` or the app's poll loop.
|
|
60
|
+
click.echo(
|
|
61
|
+
"pocketshell: `quse` is not installed on this host. "
|
|
62
|
+
"Install it via `uv tool install quse` or `pipx install quse` "
|
|
63
|
+
"and re-run.",
|
|
64
|
+
err=True,
|
|
65
|
+
)
|
|
66
|
+
return 127
|
|
67
|
+
|
|
68
|
+
completed = subprocess.run(
|
|
69
|
+
[quse_path, *args],
|
|
70
|
+
check=False,
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
)
|
|
74
|
+
# Echo verbatim so the JSON output is byte-identical to `quse --json`.
|
|
75
|
+
if completed.stdout:
|
|
76
|
+
sys.stdout.write(completed.stdout)
|
|
77
|
+
if completed.stderr:
|
|
78
|
+
sys.stderr.write(completed.stderr)
|
|
79
|
+
return completed.returncode
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.command(
|
|
83
|
+
context_settings={"help_option_names": ["-h", "--help"], "ignore_unknown_options": True},
|
|
84
|
+
)
|
|
85
|
+
@click.argument("provider", required=False)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--json",
|
|
88
|
+
"json_output",
|
|
89
|
+
is_flag=True,
|
|
90
|
+
help="Emit machine-readable JSON output identical to `quse --json`.",
|
|
91
|
+
)
|
|
92
|
+
@click.pass_context
|
|
93
|
+
def usage_command(
|
|
94
|
+
ctx: click.Context,
|
|
95
|
+
provider: Optional[str] = None,
|
|
96
|
+
json_output: bool = False,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Report quota / usage for coding-agent providers on this host.
|
|
99
|
+
|
|
100
|
+
Delegates to the `quse` CLI via subprocess. Output shape (both human
|
|
101
|
+
and JSON) is byte-identical to `quse [provider] [--json]` so any
|
|
102
|
+
consumer that already parses `quse` output keeps working when the
|
|
103
|
+
PocketShell app routes through `pocketshell usage` instead.
|
|
104
|
+
"""
|
|
105
|
+
args: list[str] = []
|
|
106
|
+
if provider:
|
|
107
|
+
args.append(provider)
|
|
108
|
+
if json_output:
|
|
109
|
+
args.append("--json")
|
|
110
|
+
exit_code = _run_quse(args)
|
|
111
|
+
# Click ignores the return value of a callback by default; we need to
|
|
112
|
+
# explicitly propagate non-zero exit codes through `ctx.exit` so the
|
|
113
|
+
# outer `main()` (and the OS) sees the same exit code `quse` reported.
|
|
114
|
+
if exit_code != 0:
|
|
115
|
+
ctx.exit(exit_code)
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Unit tests for `pocketshell usage`.
|
|
2
|
+
|
|
3
|
+
The first PR exercises:
|
|
4
|
+
- `pocketshell --help` lists the `usage` subcommand.
|
|
5
|
+
- `pocketshell usage --help` shows the click usage line.
|
|
6
|
+
- `pocketshell usage --json` forwards through to `quse --json`.
|
|
7
|
+
- `pocketshell usage <provider>` forwards the positional arg.
|
|
8
|
+
- Missing `quse` produces a friendly stderr message + exit 127.
|
|
9
|
+
- stdout/stderr/exit-code from the subprocess are proxied verbatim.
|
|
10
|
+
|
|
11
|
+
The tests stub `pocketshell.usage._resolve_quse_binary` and
|
|
12
|
+
`subprocess.run` so they never invoke a real `quse` binary; the contract
|
|
13
|
+
under test is "pocketshell delegates correctly to whatever quse exists
|
|
14
|
+
on the host", not "the provider check works".
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import subprocess
|
|
21
|
+
from typing import Sequence
|
|
22
|
+
from unittest.mock import patch
|
|
23
|
+
|
|
24
|
+
from click.testing import CliRunner
|
|
25
|
+
|
|
26
|
+
from pocketshell.cli import cli, main
|
|
27
|
+
from pocketshell.usage import usage_command
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fake_completed(
|
|
31
|
+
stdout: str = "",
|
|
32
|
+
stderr: str = "",
|
|
33
|
+
returncode: int = 0,
|
|
34
|
+
) -> subprocess.CompletedProcess:
|
|
35
|
+
return subprocess.CompletedProcess(
|
|
36
|
+
args=[],
|
|
37
|
+
returncode=returncode,
|
|
38
|
+
stdout=stdout,
|
|
39
|
+
stderr=stderr,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fake_json_payload() -> str:
|
|
44
|
+
return json.dumps(
|
|
45
|
+
{
|
|
46
|
+
"claude": {
|
|
47
|
+
"plan": "Pro",
|
|
48
|
+
"session_used_percent": 12.5,
|
|
49
|
+
"session_window_resets_at": "2026-05-27T15:00:00Z",
|
|
50
|
+
},
|
|
51
|
+
"codex": {
|
|
52
|
+
"plan": "Plus",
|
|
53
|
+
"session_used_percent": 4.0,
|
|
54
|
+
"session_window_resets_at": "2026-05-27T16:00:00Z",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
indent=2,
|
|
58
|
+
sort_keys=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_top_level_help_lists_usage_subcommand() -> None:
|
|
63
|
+
runner = CliRunner()
|
|
64
|
+
result = runner.invoke(cli, ["--help"])
|
|
65
|
+
assert result.exit_code == 0, result.output
|
|
66
|
+
assert "usage" in result.output
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_usage_help_does_not_call_quse() -> None:
|
|
70
|
+
runner = CliRunner()
|
|
71
|
+
with patch("pocketshell.usage.subprocess.run") as run, patch(
|
|
72
|
+
"pocketshell.usage._resolve_quse_binary", return_value="/fake/quse"
|
|
73
|
+
):
|
|
74
|
+
result = runner.invoke(usage_command, ["--help"])
|
|
75
|
+
assert result.exit_code == 0, result.output
|
|
76
|
+
assert "provider" in result.output.lower()
|
|
77
|
+
run.assert_not_called()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_usage_json_forwards_to_quse_and_proxies_stdout() -> None:
|
|
81
|
+
payload = _fake_json_payload()
|
|
82
|
+
runner = CliRunner()
|
|
83
|
+
with patch("pocketshell.usage._resolve_quse_binary", return_value="/fake/quse"), patch(
|
|
84
|
+
"pocketshell.usage.subprocess.run",
|
|
85
|
+
return_value=_fake_completed(stdout=payload),
|
|
86
|
+
) as run:
|
|
87
|
+
result = runner.invoke(usage_command, ["--json"])
|
|
88
|
+
assert result.exit_code == 0, result.output
|
|
89
|
+
# The stdout we got back must be the exact bytes `quse --json` emitted.
|
|
90
|
+
assert result.output == payload
|
|
91
|
+
# Args forwarded to the quse subprocess must include `--json`.
|
|
92
|
+
call_args = run.call_args
|
|
93
|
+
invoked: Sequence[str] = call_args.args[0]
|
|
94
|
+
assert invoked == ["/fake/quse", "--json"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_usage_forwards_provider_argument() -> None:
|
|
98
|
+
runner = CliRunner()
|
|
99
|
+
with patch("pocketshell.usage._resolve_quse_binary", return_value="/fake/quse"), patch(
|
|
100
|
+
"pocketshell.usage.subprocess.run",
|
|
101
|
+
return_value=_fake_completed(stdout="claude — 12.5% used\n"),
|
|
102
|
+
) as run:
|
|
103
|
+
result = runner.invoke(usage_command, ["claude"])
|
|
104
|
+
assert result.exit_code == 0, result.output
|
|
105
|
+
invoked: Sequence[str] = run.call_args.args[0]
|
|
106
|
+
assert invoked == ["/fake/quse", "claude"]
|
|
107
|
+
assert "claude" in result.output
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_usage_forwards_provider_and_json_flag_together() -> None:
|
|
111
|
+
runner = CliRunner()
|
|
112
|
+
with patch("pocketshell.usage._resolve_quse_binary", return_value="/fake/quse"), patch(
|
|
113
|
+
"pocketshell.usage.subprocess.run",
|
|
114
|
+
return_value=_fake_completed(stdout="{\n \"claude\": {}\n}\n"),
|
|
115
|
+
) as run:
|
|
116
|
+
result = runner.invoke(usage_command, ["claude", "--json"])
|
|
117
|
+
assert result.exit_code == 0, result.output
|
|
118
|
+
invoked: Sequence[str] = run.call_args.args[0]
|
|
119
|
+
assert invoked == ["/fake/quse", "claude", "--json"]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_usage_returns_127_when_quse_missing() -> None:
|
|
123
|
+
runner = CliRunner()
|
|
124
|
+
with patch("pocketshell.usage._resolve_quse_binary", return_value=None), patch(
|
|
125
|
+
"pocketshell.usage.subprocess.run"
|
|
126
|
+
) as run:
|
|
127
|
+
result = runner.invoke(usage_command, ["--json"], catch_exceptions=False)
|
|
128
|
+
# 127 is the POSIX exit code for "command not found" and matches the
|
|
129
|
+
# signal `UsageRemoteSource.fetchUsage` already special-cases on the
|
|
130
|
+
# Kotlin side.
|
|
131
|
+
assert result.exit_code == 127, result.output
|
|
132
|
+
# The friendly message lands on stderr; CliRunner mixes stdout+stderr
|
|
133
|
+
# by default but `mix_stderr=False` is removed in click>=8.2, so just
|
|
134
|
+
# check that the install hint was emitted somewhere.
|
|
135
|
+
assert "quse" in result.output.lower()
|
|
136
|
+
run.assert_not_called()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_usage_proxies_nonzero_exit_from_quse() -> None:
|
|
140
|
+
runner = CliRunner()
|
|
141
|
+
with patch("pocketshell.usage._resolve_quse_binary", return_value="/fake/quse"), patch(
|
|
142
|
+
"pocketshell.usage.subprocess.run",
|
|
143
|
+
return_value=_fake_completed(stderr="error: unknown provider\n", returncode=2),
|
|
144
|
+
):
|
|
145
|
+
result = runner.invoke(usage_command, ["wat"])
|
|
146
|
+
assert result.exit_code == 2
|
|
147
|
+
# stderr from the subprocess must reach the user (otherwise debugging
|
|
148
|
+
# a failing provider would be opaque).
|
|
149
|
+
assert "unknown provider" in result.output
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_main_returns_int_on_success() -> None:
|
|
153
|
+
"""`main` is the canonical entrypoint for the console-script and
|
|
154
|
+
`python -m pocketshell`. It must translate Click's exit-code object
|
|
155
|
+
into a plain int and never raise SystemExit through to the caller.
|
|
156
|
+
"""
|
|
157
|
+
with patch("pocketshell.usage._resolve_quse_binary", return_value="/fake/quse"), patch(
|
|
158
|
+
"pocketshell.usage.subprocess.run",
|
|
159
|
+
return_value=_fake_completed(stdout=_fake_json_payload()),
|
|
160
|
+
):
|
|
161
|
+
exit_code = main(["usage", "--json"])
|
|
162
|
+
assert isinstance(exit_code, int)
|
|
163
|
+
assert exit_code == 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_main_returns_nonzero_on_unknown_subcommand() -> None:
|
|
167
|
+
exit_code = main(["bogus-subcommand-that-does-not-exist"])
|
|
168
|
+
assert exit_code != 0
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[options]
|
|
6
|
+
exclude-newer = "2026-05-20T13:56:21.125647543Z"
|
|
7
|
+
exclude-newer-span = "P7D"
|
|
8
|
+
|
|
9
|
+
[[package]]
|
|
10
|
+
name = "click"
|
|
11
|
+
version = "8.4.0"
|
|
12
|
+
source = { registry = "https://pypi.org/simple" }
|
|
13
|
+
dependencies = [
|
|
14
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
15
|
+
]
|
|
16
|
+
sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" }
|
|
17
|
+
wheels = [
|
|
18
|
+
{ url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[package]]
|
|
22
|
+
name = "colorama"
|
|
23
|
+
version = "0.4.6"
|
|
24
|
+
source = { registry = "https://pypi.org/simple" }
|
|
25
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
26
|
+
wheels = [
|
|
27
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[package]]
|
|
31
|
+
name = "iniconfig"
|
|
32
|
+
version = "2.3.0"
|
|
33
|
+
source = { registry = "https://pypi.org/simple" }
|
|
34
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
35
|
+
wheels = [
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "packaging"
|
|
41
|
+
version = "26.2"
|
|
42
|
+
source = { registry = "https://pypi.org/simple" }
|
|
43
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
44
|
+
wheels = [
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[[package]]
|
|
49
|
+
name = "pluggy"
|
|
50
|
+
version = "1.6.0"
|
|
51
|
+
source = { registry = "https://pypi.org/simple" }
|
|
52
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
53
|
+
wheels = [
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[[package]]
|
|
58
|
+
name = "pocketshell-cli"
|
|
59
|
+
version = "0.1.0"
|
|
60
|
+
source = { editable = "." }
|
|
61
|
+
dependencies = [
|
|
62
|
+
{ name = "click" },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[package.optional-dependencies]
|
|
66
|
+
dev = [
|
|
67
|
+
{ name = "pytest" },
|
|
68
|
+
{ name = "ruff" },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[package.dev-dependencies]
|
|
72
|
+
dev = [
|
|
73
|
+
{ name = "pytest" },
|
|
74
|
+
{ name = "ruff" },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[package.metadata]
|
|
78
|
+
requires-dist = [
|
|
79
|
+
{ name = "click", specifier = ">=8.2.0" },
|
|
80
|
+
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.0" },
|
|
81
|
+
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" },
|
|
82
|
+
]
|
|
83
|
+
provides-extras = ["dev"]
|
|
84
|
+
|
|
85
|
+
[package.metadata.requires-dev]
|
|
86
|
+
dev = [
|
|
87
|
+
{ name = "pytest", specifier = ">=8.4.0" },
|
|
88
|
+
{ name = "ruff", specifier = ">=0.15.0" },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[[package]]
|
|
92
|
+
name = "pygments"
|
|
93
|
+
version = "2.20.0"
|
|
94
|
+
source = { registry = "https://pypi.org/simple" }
|
|
95
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
96
|
+
wheels = [
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[[package]]
|
|
101
|
+
name = "pytest"
|
|
102
|
+
version = "9.0.3"
|
|
103
|
+
source = { registry = "https://pypi.org/simple" }
|
|
104
|
+
dependencies = [
|
|
105
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
106
|
+
{ name = "iniconfig" },
|
|
107
|
+
{ name = "packaging" },
|
|
108
|
+
{ name = "pluggy" },
|
|
109
|
+
{ name = "pygments" },
|
|
110
|
+
]
|
|
111
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
112
|
+
wheels = [
|
|
113
|
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[[package]]
|
|
117
|
+
name = "ruff"
|
|
118
|
+
version = "0.15.13"
|
|
119
|
+
source = { registry = "https://pypi.org/simple" }
|
|
120
|
+
sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" }
|
|
121
|
+
wheels = [
|
|
122
|
+
{ url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" },
|
|
123
|
+
{ url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" },
|
|
124
|
+
{ url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" },
|
|
125
|
+
{ url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" },
|
|
126
|
+
{ url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" },
|
|
127
|
+
{ url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" },
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" },
|
|
129
|
+
{ url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" },
|
|
130
|
+
{ url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" },
|
|
131
|
+
{ url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" },
|
|
132
|
+
{ url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" },
|
|
133
|
+
{ url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" },
|
|
134
|
+
{ url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" },
|
|
135
|
+
{ url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" },
|
|
136
|
+
{ url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" },
|
|
137
|
+
{ url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" },
|
|
138
|
+
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
|
|
139
|
+
]
|