pushmetrics-cli 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.
- pushmetrics_cli-0.1.0/MANIFEST.in +12 -0
- pushmetrics_cli-0.1.0/PKG-INFO +221 -0
- pushmetrics_cli-0.1.0/README.md +201 -0
- pushmetrics_cli-0.1.0/pm/__init__.py +1 -0
- pushmetrics_cli-0.1.0/pm/__main__.py +4 -0
- pushmetrics_cli-0.1.0/pm/_alias.py +164 -0
- pushmetrics_cli-0.1.0/pm/auth/__init__.py +0 -0
- pushmetrics_cli-0.1.0/pm/auth/device_flow.py +187 -0
- pushmetrics_cli-0.1.0/pm/auth/exchange.py +79 -0
- pushmetrics_cli-0.1.0/pm/auth/jwt_cache.py +163 -0
- pushmetrics_cli-0.1.0/pm/auth/secret_files.py +58 -0
- pushmetrics_cli-0.1.0/pm/auth/session.py +440 -0
- pushmetrics_cli-0.1.0/pm/auth/store.py +160 -0
- pushmetrics_cli-0.1.0/pm/auth/token.py +289 -0
- pushmetrics_cli-0.1.0/pm/cli.py +282 -0
- pushmetrics_cli-0.1.0/pm/client/__init__.py +68 -0
- pushmetrics_cli-0.1.0/pm/client/access_tokens.py +54 -0
- pushmetrics_cli-0.1.0/pm/client/base.py +229 -0
- pushmetrics_cli-0.1.0/pm/client/blocks.py +461 -0
- pushmetrics_cli-0.1.0/pm/client/databases.py +147 -0
- pushmetrics_cli-0.1.0/pm/client/favorites.py +14 -0
- pushmetrics_cli-0.1.0/pm/client/folders.py +10 -0
- pushmetrics_cli-0.1.0/pm/client/integrations.py +14 -0
- pushmetrics_cli-0.1.0/pm/client/organizations.py +20 -0
- pushmetrics_cli-0.1.0/pm/client/recipients.py +92 -0
- pushmetrics_cli-0.1.0/pm/client/reports.py +148 -0
- pushmetrics_cli-0.1.0/pm/client/roles.py +100 -0
- pushmetrics_cli-0.1.0/pm/client/runs.py +134 -0
- pushmetrics_cli-0.1.0/pm/client/saved_blocks.py +13 -0
- pushmetrics_cli-0.1.0/pm/client/tableau.py +108 -0
- pushmetrics_cli-0.1.0/pm/client/users.py +71 -0
- pushmetrics_cli-0.1.0/pm/client/webhooks.py +43 -0
- pushmetrics_cli-0.1.0/pm/client/workspaces.py +34 -0
- pushmetrics_cli-0.1.0/pm/commands/__init__.py +0 -0
- pushmetrics_cli-0.1.0/pm/commands/_block_factory.py +762 -0
- pushmetrics_cli-0.1.0/pm/commands/_helpers.py +178 -0
- pushmetrics_cli-0.1.0/pm/commands/auth.py +222 -0
- pushmetrics_cli-0.1.0/pm/commands/block.py +26 -0
- pushmetrics_cli-0.1.0/pm/commands/chart.py +20 -0
- pushmetrics_cli-0.1.0/pm/commands/db.py +291 -0
- pushmetrics_cli-0.1.0/pm/commands/email.py +13 -0
- pushmetrics_cli-0.1.0/pm/commands/explore.py +20 -0
- pushmetrics_cli-0.1.0/pm/commands/favorites.py +77 -0
- pushmetrics_cli-0.1.0/pm/commands/folders.py +113 -0
- pushmetrics_cli-0.1.0/pm/commands/help.py +643 -0
- pushmetrics_cli-0.1.0/pm/commands/integrations.py +95 -0
- pushmetrics_cli-0.1.0/pm/commands/metrics_view.py +195 -0
- pushmetrics_cli-0.1.0/pm/commands/organizations.py +56 -0
- pushmetrics_cli-0.1.0/pm/commands/recipient.py +226 -0
- pushmetrics_cli-0.1.0/pm/commands/reports.py +364 -0
- pushmetrics_cli-0.1.0/pm/commands/roles.py +102 -0
- pushmetrics_cli-0.1.0/pm/commands/runs.py +214 -0
- pushmetrics_cli-0.1.0/pm/commands/saved_blocks.py +97 -0
- pushmetrics_cli-0.1.0/pm/commands/sftp.py +19 -0
- pushmetrics_cli-0.1.0/pm/commands/slack.py +13 -0
- pushmetrics_cli-0.1.0/pm/commands/slides.py +216 -0
- pushmetrics_cli-0.1.0/pm/commands/sql.py +17 -0
- pushmetrics_cli-0.1.0/pm/commands/tableau.py +148 -0
- pushmetrics_cli-0.1.0/pm/commands/token.py +107 -0
- pushmetrics_cli-0.1.0/pm/commands/users.py +262 -0
- pushmetrics_cli-0.1.0/pm/commands/webhooks.py +132 -0
- pushmetrics_cli-0.1.0/pm/commands/workspace.py +156 -0
- pushmetrics_cli-0.1.0/pm/config.py +192 -0
- pushmetrics_cli-0.1.0/pm/errors.py +228 -0
- pushmetrics_cli-0.1.0/pm/log.py +63 -0
- pushmetrics_cli-0.1.0/pm/models/__init__.py +31 -0
- pushmetrics_cli-0.1.0/pm/models/access_tokens.py +25 -0
- pushmetrics_cli-0.1.0/pm/models/blocks.py +21 -0
- pushmetrics_cli-0.1.0/pm/models/databases.py +15 -0
- pushmetrics_cli-0.1.0/pm/models/favorites.py +14 -0
- pushmetrics_cli-0.1.0/pm/models/folders.py +14 -0
- pushmetrics_cli-0.1.0/pm/models/integrations.py +20 -0
- pushmetrics_cli-0.1.0/pm/models/organizations.py +13 -0
- pushmetrics_cli-0.1.0/pm/models/recipients.py +41 -0
- pushmetrics_cli-0.1.0/pm/models/reports.py +16 -0
- pushmetrics_cli-0.1.0/pm/models/roles.py +22 -0
- pushmetrics_cli-0.1.0/pm/models/runs.py +56 -0
- pushmetrics_cli-0.1.0/pm/models/saved_blocks.py +15 -0
- pushmetrics_cli-0.1.0/pm/models/users.py +36 -0
- pushmetrics_cli-0.1.0/pm/models/webhooks.py +23 -0
- pushmetrics_cli-0.1.0/pm/models/workspaces.py +16 -0
- pushmetrics_cli-0.1.0/pm/output.py +92 -0
- pushmetrics_cli-0.1.0/pm/types.py +130 -0
- pushmetrics_cli-0.1.0/pm/validators.py +499 -0
- pushmetrics_cli-0.1.0/pushmetrics_cli.egg-info/PKG-INFO +221 -0
- pushmetrics_cli-0.1.0/pushmetrics_cli.egg-info/SOURCES.txt +138 -0
- pushmetrics_cli-0.1.0/pushmetrics_cli.egg-info/dependency_links.txt +1 -0
- pushmetrics_cli-0.1.0/pushmetrics_cli.egg-info/entry_points.txt +2 -0
- pushmetrics_cli-0.1.0/pushmetrics_cli.egg-info/requires.txt +16 -0
- pushmetrics_cli-0.1.0/pushmetrics_cli.egg-info/top_level.txt +1 -0
- pushmetrics_cli-0.1.0/pyproject.toml +63 -0
- pushmetrics_cli-0.1.0/setup.cfg +4 -0
- pushmetrics_cli-0.1.0/tests/__init__.py +0 -0
- pushmetrics_cli-0.1.0/tests/_runner.py +22 -0
- pushmetrics_cli-0.1.0/tests/conftest.py +66 -0
- pushmetrics_cli-0.1.0/tests/test_agent_profile.py +72 -0
- pushmetrics_cli-0.1.0/tests/test_aliases.py +135 -0
- pushmetrics_cli-0.1.0/tests/test_block_factory.py +873 -0
- pushmetrics_cli-0.1.0/tests/test_client_access_tokens.py +51 -0
- pushmetrics_cli-0.1.0/tests/test_client_blocks.py +743 -0
- pushmetrics_cli-0.1.0/tests/test_client_favorites.py +41 -0
- pushmetrics_cli-0.1.0/tests/test_client_folders.py +61 -0
- pushmetrics_cli-0.1.0/tests/test_client_integrations.py +69 -0
- pushmetrics_cli-0.1.0/tests/test_client_organizations.py +45 -0
- pushmetrics_cli-0.1.0/tests/test_client_recipients.py +96 -0
- pushmetrics_cli-0.1.0/tests/test_client_reports.py +238 -0
- pushmetrics_cli-0.1.0/tests/test_client_runs.py +170 -0
- pushmetrics_cli-0.1.0/tests/test_client_saved_blocks.py +47 -0
- pushmetrics_cli-0.1.0/tests/test_client_tableau.py +118 -0
- pushmetrics_cli-0.1.0/tests/test_client_users.py +50 -0
- pushmetrics_cli-0.1.0/tests/test_client_webhooks.py +58 -0
- pushmetrics_cli-0.1.0/tests/test_credential_safety.py +200 -0
- pushmetrics_cli-0.1.0/tests/test_device_flow.py +127 -0
- pushmetrics_cli-0.1.0/tests/test_did_you_mean.py +201 -0
- pushmetrics_cli-0.1.0/tests/test_domain_groups.py +193 -0
- pushmetrics_cli-0.1.0/tests/test_errors.py +165 -0
- pushmetrics_cli-0.1.0/tests/test_first_run.py +372 -0
- pushmetrics_cli-0.1.0/tests/test_hardening.py +254 -0
- pushmetrics_cli-0.1.0/tests/test_help_command.py +433 -0
- pushmetrics_cli-0.1.0/tests/test_integration_commands.py +146 -0
- pushmetrics_cli-0.1.0/tests/test_jwt_cache.py +337 -0
- pushmetrics_cli-0.1.0/tests/test_keyring_namespace.py +245 -0
- pushmetrics_cli-0.1.0/tests/test_list_limit.py +154 -0
- pushmetrics_cli-0.1.0/tests/test_metrics_view_commands.py +209 -0
- pushmetrics_cli-0.1.0/tests/test_no_backend_imports.py +145 -0
- pushmetrics_cli-0.1.0/tests/test_packaging.py +106 -0
- pushmetrics_cli-0.1.0/tests/test_pm_client.py +60 -0
- pushmetrics_cli-0.1.0/tests/test_prod_run_fixes.py +237 -0
- pushmetrics_cli-0.1.0/tests/test_profile_pat_only.py +138 -0
- pushmetrics_cli-0.1.0/tests/test_quiet_flag.py +61 -0
- pushmetrics_cli-0.1.0/tests/test_recipient_commands.py +318 -0
- pushmetrics_cli-0.1.0/tests/test_report_preview_commands.py +36 -0
- pushmetrics_cli-0.1.0/tests/test_secret_files.py +106 -0
- pushmetrics_cli-0.1.0/tests/test_session.py +298 -0
- pushmetrics_cli-0.1.0/tests/test_slides_commands.py +244 -0
- pushmetrics_cli-0.1.0/tests/test_tableau_commands.py +200 -0
- pushmetrics_cli-0.1.0/tests/test_token_auth.py +627 -0
- pushmetrics_cli-0.1.0/tests/test_token_commands.py +192 -0
- pushmetrics_cli-0.1.0/tests/test_types.py +159 -0
- pushmetrics_cli-0.1.0/tests/test_validators.py +815 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# What the sdist carries beyond the `pm` package and the metadata files
|
|
2
|
+
# setuptools adds on its own. The wheel is governed by packages.find in
|
|
3
|
+
# pyproject.toml and never includes any of this.
|
|
4
|
+
#
|
|
5
|
+
# tests/ ships complete (conftest, package marker, helpers) so the sdist is
|
|
6
|
+
# testable as downloaded; setuptools' default would copy only tests/test_*.py.
|
|
7
|
+
# tests/repo_only/ needs the monorepo (it reads backend files by path and
|
|
8
|
+
# hard-fails without them), so it is repo-only: see its __init__.py.
|
|
9
|
+
recursive-include tests *.py
|
|
10
|
+
prune tests/repo_only
|
|
11
|
+
# Developer-only live-backend exercisers. Never part of the distribution.
|
|
12
|
+
prune scripts
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pushmetrics-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for PushMetrics
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Requires-Dist: requests>=2.31
|
|
9
|
+
Requires-Dist: keyring>=24.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
16
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
17
|
+
Requires-Dist: setuptools>=68; extra == "dev"
|
|
18
|
+
Requires-Dist: wheel; extra == "dev"
|
|
19
|
+
Requires-Dist: pyyaml>=6; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# PushMetrics CLI (`pm`)
|
|
22
|
+
|
|
23
|
+
`pm` is the command-line client for [PushMetrics](https://pushmetrics.io). It talks to the same REST API the web app uses, with the same permissions: you see the reports, databases and users you have access to, in one workspace, nothing more.
|
|
24
|
+
|
|
25
|
+
Use it to run and debug reports, keep report definitions in git, feed CI pipelines, or give an AI coding agent a scriptable handle on your workspace. Every command has a `--json` mode.
|
|
26
|
+
|
|
27
|
+
The complete user guide is on the website: [PushMetrics CLI](https://pushmetrics.io/docs/admin/cli/).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
> **PLACEHOLDER (D1).** The install command depends on the distribution channel and is filled in at release time.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# PLACEHOLDER (D1): install command goes here
|
|
35
|
+
pm --version
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.9 or newer. Runs on macOS, Linux and Windows.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
1. In the PushMetrics app, open **Settings → Access Tokens** (`/a/tokens`), click **New token**, and copy the `pmpat_...` value. It is shown once, and it is bound to the workspace that was active when you created it.
|
|
43
|
+
2. Log in with the token. `pm` reads it from a hidden prompt, or from stdin when piped, and stores it in your OS credential store:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pm login --token
|
|
47
|
+
pm whoami
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
3. Use it:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pm report list
|
|
54
|
+
pm report get <report-uid>
|
|
55
|
+
pm report run <report-uid> --send-to-me
|
|
56
|
+
pm run list --last-days 2 --status failed
|
|
57
|
+
pm run logs <run-uid> --level ERROR
|
|
58
|
+
pm report list --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
In CI, skip `pm login` and set `PM_API_TOKEN` instead.
|
|
62
|
+
|
|
63
|
+
> **PLACEHOLDER (D2).** Browser login (`pm login` without `--token`, Auth0 device flow) is documented here only if it ships in the first release.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
`pm --help` lists every command. `pm <group> --help` lists a group's actions, `pm <group> <action> --help` the options, and `pm help <group>` shows a compact page with examples.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Account:
|
|
71
|
+
login Log in with a personal access token.
|
|
72
|
+
logout Log out and forget the stored token or session.
|
|
73
|
+
whoami Show who you are logged in as, and the workspace.
|
|
74
|
+
token Personal access tokens for API / CLI authentication.
|
|
75
|
+
config Profiles: which PushMetrics server pm talks to.
|
|
76
|
+
|
|
77
|
+
Commands:
|
|
78
|
+
block Blocks of any type.
|
|
79
|
+
chart Charts (Plotly blocks).
|
|
80
|
+
database Databases, their schemas and tables.
|
|
81
|
+
email Email blocks.
|
|
82
|
+
explore Explore blocks (saved data views).
|
|
83
|
+
favorite Your favorite reports and other objects.
|
|
84
|
+
folder Folders that organize reports.
|
|
85
|
+
help Show compact help for a group, with examples.
|
|
86
|
+
integration Connected services (Slack, Tableau, Google Drive, ...).
|
|
87
|
+
metrics-view Metrics views (the semantic layer).
|
|
88
|
+
organization Your organization (the owner of workspaces).
|
|
89
|
+
recipient Recipients: who reports are delivered to.
|
|
90
|
+
report Reports and their runs.
|
|
91
|
+
role Roles and groups in the current workspace.
|
|
92
|
+
run Report runs: status, results and logs.
|
|
93
|
+
saved-block Saved blocks (reusable block templates).
|
|
94
|
+
sftp SFTP upload blocks.
|
|
95
|
+
slack Slack message blocks.
|
|
96
|
+
slides Google Slides blocks.
|
|
97
|
+
sql SQL query blocks.
|
|
98
|
+
tableau Tableau view blocks.
|
|
99
|
+
user Users in the current workspace.
|
|
100
|
+
webhook Webhooks that trigger report runs.
|
|
101
|
+
workspace Workspaces you belong to.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The ones most people need:
|
|
105
|
+
|
|
106
|
+
| Command | What it does |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `pm whoami` | Your user and the token's workspace. |
|
|
109
|
+
| `pm report list` | Reports in the workspace. `--name` filters by name, `--limit` caps the rows. |
|
|
110
|
+
| `pm report get <uid>` | One report. Full record with `--json`. |
|
|
111
|
+
| `pm report yaml <uid>` | The report's full YAML configuration, to stdout. |
|
|
112
|
+
| `pm report apply <file> --uid <uid>` | Write a YAML file back to a report. `--dry-run` shows the diff without saving. |
|
|
113
|
+
| `pm report run <uid>` | Start a run and print its uid. `--send-to-me` sends the email blocks to you and skips every other destination; `--email <address>` previews as a saved recipient. |
|
|
114
|
+
| `pm run list` | Recent runs, newest first. `--report <uid>`, `--last-days N`, `--status failed`, `--limit N`. |
|
|
115
|
+
| `pm run get <run-uid>` | A run's status and timing. |
|
|
116
|
+
| `pm run logs <run-uid>` | A run's log lines. `--level ERROR` keeps only errors. |
|
|
117
|
+
| `pm run results <run-uid>` | What each block of the run produced. |
|
|
118
|
+
| `pm token list` / `create <name>` / `revoke <id>` | Manage your personal access tokens. |
|
|
119
|
+
| `pm config list` | Profiles and the environment overrides in effect. |
|
|
120
|
+
|
|
121
|
+
`--json` works before or after the action (`pm --json report list` and `pm report list --json`). Data goes to stdout; messages and errors go to stderr. `pm` exits with `0` on success, `1` when a command fails and `2` when the command line is wrong.
|
|
122
|
+
|
|
123
|
+
### Global options
|
|
124
|
+
|
|
125
|
+
| Option | Meaning |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `--json` | Print JSON instead of tables. |
|
|
128
|
+
| `--quiet` | Suppress informational messages on stderr. Errors still print. |
|
|
129
|
+
| `-v`, `--verbose` | Log each request, its status and timing to stderr; response bodies on errors only. Tokens are never logged. |
|
|
130
|
+
| `--profile <name>` | Use a profile other than `default`. |
|
|
131
|
+
| `--api-url <url>` | Talk to a different PushMetrics address for one command. |
|
|
132
|
+
| `--version` | Print the version and exit. |
|
|
133
|
+
|
|
134
|
+
### Environment variables
|
|
135
|
+
|
|
136
|
+
| Variable | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `PM_API_TOKEN` | A personal access token. Wins over a token stored by `pm login --token`. For CI and agents. |
|
|
139
|
+
| `PM_PROFILE` | Profile to use when no `--profile` is given. |
|
|
140
|
+
| `PM_API_URL` | Talk to this address instead of the profile's. |
|
|
141
|
+
| `PM_CONFIG_HOME` | Where configuration and fallback credential files live. Default: `~/.pm`. |
|
|
142
|
+
|
|
143
|
+
## Several workspaces
|
|
144
|
+
|
|
145
|
+
A token works in one workspace. To work in a second one, create a token there and give it its own profile:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pm config set acme api_url https://pushmetrics.io
|
|
149
|
+
pm --profile acme login --token
|
|
150
|
+
pm --profile acme report list
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`pm workspace switch` is for browser login only; with a token, the token decides the workspace.
|
|
154
|
+
|
|
155
|
+
## Where credentials live
|
|
156
|
+
|
|
157
|
+
`pm login --token` stores the token in the OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service) under the service name `pushmetrics-cli-pat`, one entry per profile. Without a usable credential store it falls back to `~/.pm/tokens.json`, mode `0600`.
|
|
158
|
+
|
|
159
|
+
`pm` also caches the short-lived access token it exchanges from your personal access token, under the service name `pushmetrics-cli-jwt` or in `~/.pm/jwt_cache.json` with mode `0600`.
|
|
160
|
+
|
|
161
|
+
`pm logout` removes both for the active profile. It does not revoke the token on the server; use `pm token revoke` or the Access Tokens page for that.
|
|
162
|
+
|
|
163
|
+
`pm` never takes a token on the command line and never prints one, in `-v` output or in errors. It only sends tokens over `https://`, except to `localhost`.
|
|
164
|
+
|
|
165
|
+
## Troubleshooting
|
|
166
|
+
|
|
167
|
+
**`Not logged in. Run pm login --token with a personal access token, or set PM_API_TOKEN.`** Nothing stored for the active profile and `PM_API_TOKEN` unset. Tokens are stored per profile: check `pm config list`.
|
|
168
|
+
|
|
169
|
+
**`That does not look like a personal access token…`** What was pasted, piped or set in `PM_API_TOKEN` is not a `pmpat_...` value. Copy it again exactly.
|
|
170
|
+
|
|
171
|
+
**`Personal access token rejected (invalid, expired, or revoked).`** Create a new token and log in again.
|
|
172
|
+
|
|
173
|
+
**`Could not reach https://... (ConnectionError)…`** Network, or the wrong address: `pm config list` shows it.
|
|
174
|
+
|
|
175
|
+
**`Unknown profile: 'acme'…`** or **`Profile 'acme' has no api_url…`** Each message ends with the `pm config set` command that fixes it; see also [Several workspaces](#several-workspaces).
|
|
176
|
+
|
|
177
|
+
**`API ... failed: 403` / `404` with a `Hint:` line.** The hint names the next step: permissions in the token's workspace, or an id that is not visible there.
|
|
178
|
+
|
|
179
|
+
**Anything else.** Re-run with `pm -v ...` and look at the response status and body.
|
|
180
|
+
|
|
181
|
+
The [website guide](https://pushmetrics.io/docs/admin/cli/) lists every error with its fix.
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
The CLI lives in the `cli/` directory of the PushMetrics monorepo and must not import anything from the backend packages (`superset/`, `prefect/`); a test enforces this. To work on it:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
cd cli
|
|
189
|
+
python -m venv .venv && source .venv/bin/activate
|
|
190
|
+
pip install -e '.[dev]' # contributors only; users install the released package
|
|
191
|
+
pytest tests/ -v
|
|
192
|
+
ruff check .
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Tests mock HTTP with the `responses` library and never reach a real backend. `tests/repo_only/` holds tests that read files outside `cli/`, such as `test_docs_commands_exist.py`, which checks every `pm` command written in this README and in the website guide against the real command tree; they run from a checkout and are not shipped in the sdist. CI runs the suite on every pull request that touches `cli/`.
|
|
196
|
+
|
|
197
|
+
When you run a real `pm` process during development, set `PM_CONFIG_HOME` to a scratch directory **and** `PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring`. `PM_CONFIG_HOME` alone does not isolate credentials: the OS keyring is keyed by profile name, so a scratch run would read, and `pm logout` would delete, your real stored token.
|
|
198
|
+
|
|
199
|
+
**Adding a resource.** For anything with the standard CRUD shape (`/<resource>/` list, `/<resource>/<uid>` single, plus `POST`/`PUT`/`DELETE`):
|
|
200
|
+
|
|
201
|
+
1. **Model** in `pm/models/<resource>.py`: a Pydantic `BaseModel` with `extra="allow"`; re-export from `pm/models/__init__.py`.
|
|
202
|
+
2. **Client** in `pm/client/<resource>.py`: subclass `CrudClient[Model]`, set `base_path` and `model`. Register it on the `PMClient` facade in `pm/client/__init__.py`.
|
|
203
|
+
3. **Commands** in `pm/commands/<resource>.py`: a `click.group(cls=AliasedGroup, aliases={"ls": "list"})` with `list/get/create/update/delete`; use `parse_data` and `merge_fields` from `pm/commands/_helpers.py` for the `--data '<json>'` pattern. Skip verbs the backend does not support.
|
|
204
|
+
4. **Wire it** in `pm/cli.py` with `cli.add_command(...)`.
|
|
205
|
+
5. **Tests** in `tests/test_client_<resource>.py`, asserting on model attributes.
|
|
206
|
+
6. **Docs**: add the command to the website guide (`website/content/posts/docs/admin/2026-10-12-cli.md`); the docs test fails if a documented command does not exist.
|
|
207
|
+
|
|
208
|
+
Endpoint paths to mirror live in `frontend/src/shared/services/*.ts`; the frontend prefixes everything with `/api/v1`.
|
|
209
|
+
|
|
210
|
+
The `PMClient` facade is reusable from Python without the command layer:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from pm.client import PMClient
|
|
214
|
+
from pm.config import get_profile
|
|
215
|
+
|
|
216
|
+
client = PMClient.from_profile(get_profile("default"))
|
|
217
|
+
for report in client.reports.list():
|
|
218
|
+
print(report.uid, report.name)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Authentication resolution order inside `PMSession.from_profile`: `PM_API_JWT` (a delegation JWT the PushMetrics AI agent sets, used as-is) > personal access token (`PM_API_TOKEN` or stored, exchanged at `/api/v1/security/exchange` for a short-lived workspace-scoped JWT) > Auth0 session cookies from a browser login. Bearer JWTs carry a `workspace_id` claim that the backend installs as the request's workspace, so a pure Bearer client has full workspace context; cookies are the interactive path only.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# PushMetrics CLI (`pm`)
|
|
2
|
+
|
|
3
|
+
`pm` is the command-line client for [PushMetrics](https://pushmetrics.io). It talks to the same REST API the web app uses, with the same permissions: you see the reports, databases and users you have access to, in one workspace, nothing more.
|
|
4
|
+
|
|
5
|
+
Use it to run and debug reports, keep report definitions in git, feed CI pipelines, or give an AI coding agent a scriptable handle on your workspace. Every command has a `--json` mode.
|
|
6
|
+
|
|
7
|
+
The complete user guide is on the website: [PushMetrics CLI](https://pushmetrics.io/docs/admin/cli/).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
> **PLACEHOLDER (D1).** The install command depends on the distribution channel and is filled in at release time.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# PLACEHOLDER (D1): install command goes here
|
|
15
|
+
pm --version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Python 3.9 or newer. Runs on macOS, Linux and Windows.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
1. In the PushMetrics app, open **Settings → Access Tokens** (`/a/tokens`), click **New token**, and copy the `pmpat_...` value. It is shown once, and it is bound to the workspace that was active when you created it.
|
|
23
|
+
2. Log in with the token. `pm` reads it from a hidden prompt, or from stdin when piped, and stores it in your OS credential store:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pm login --token
|
|
27
|
+
pm whoami
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Use it:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pm report list
|
|
34
|
+
pm report get <report-uid>
|
|
35
|
+
pm report run <report-uid> --send-to-me
|
|
36
|
+
pm run list --last-days 2 --status failed
|
|
37
|
+
pm run logs <run-uid> --level ERROR
|
|
38
|
+
pm report list --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In CI, skip `pm login` and set `PM_API_TOKEN` instead.
|
|
42
|
+
|
|
43
|
+
> **PLACEHOLDER (D2).** Browser login (`pm login` without `--token`, Auth0 device flow) is documented here only if it ships in the first release.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
`pm --help` lists every command. `pm <group> --help` lists a group's actions, `pm <group> <action> --help` the options, and `pm help <group>` shows a compact page with examples.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Account:
|
|
51
|
+
login Log in with a personal access token.
|
|
52
|
+
logout Log out and forget the stored token or session.
|
|
53
|
+
whoami Show who you are logged in as, and the workspace.
|
|
54
|
+
token Personal access tokens for API / CLI authentication.
|
|
55
|
+
config Profiles: which PushMetrics server pm talks to.
|
|
56
|
+
|
|
57
|
+
Commands:
|
|
58
|
+
block Blocks of any type.
|
|
59
|
+
chart Charts (Plotly blocks).
|
|
60
|
+
database Databases, their schemas and tables.
|
|
61
|
+
email Email blocks.
|
|
62
|
+
explore Explore blocks (saved data views).
|
|
63
|
+
favorite Your favorite reports and other objects.
|
|
64
|
+
folder Folders that organize reports.
|
|
65
|
+
help Show compact help for a group, with examples.
|
|
66
|
+
integration Connected services (Slack, Tableau, Google Drive, ...).
|
|
67
|
+
metrics-view Metrics views (the semantic layer).
|
|
68
|
+
organization Your organization (the owner of workspaces).
|
|
69
|
+
recipient Recipients: who reports are delivered to.
|
|
70
|
+
report Reports and their runs.
|
|
71
|
+
role Roles and groups in the current workspace.
|
|
72
|
+
run Report runs: status, results and logs.
|
|
73
|
+
saved-block Saved blocks (reusable block templates).
|
|
74
|
+
sftp SFTP upload blocks.
|
|
75
|
+
slack Slack message blocks.
|
|
76
|
+
slides Google Slides blocks.
|
|
77
|
+
sql SQL query blocks.
|
|
78
|
+
tableau Tableau view blocks.
|
|
79
|
+
user Users in the current workspace.
|
|
80
|
+
webhook Webhooks that trigger report runs.
|
|
81
|
+
workspace Workspaces you belong to.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The ones most people need:
|
|
85
|
+
|
|
86
|
+
| Command | What it does |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `pm whoami` | Your user and the token's workspace. |
|
|
89
|
+
| `pm report list` | Reports in the workspace. `--name` filters by name, `--limit` caps the rows. |
|
|
90
|
+
| `pm report get <uid>` | One report. Full record with `--json`. |
|
|
91
|
+
| `pm report yaml <uid>` | The report's full YAML configuration, to stdout. |
|
|
92
|
+
| `pm report apply <file> --uid <uid>` | Write a YAML file back to a report. `--dry-run` shows the diff without saving. |
|
|
93
|
+
| `pm report run <uid>` | Start a run and print its uid. `--send-to-me` sends the email blocks to you and skips every other destination; `--email <address>` previews as a saved recipient. |
|
|
94
|
+
| `pm run list` | Recent runs, newest first. `--report <uid>`, `--last-days N`, `--status failed`, `--limit N`. |
|
|
95
|
+
| `pm run get <run-uid>` | A run's status and timing. |
|
|
96
|
+
| `pm run logs <run-uid>` | A run's log lines. `--level ERROR` keeps only errors. |
|
|
97
|
+
| `pm run results <run-uid>` | What each block of the run produced. |
|
|
98
|
+
| `pm token list` / `create <name>` / `revoke <id>` | Manage your personal access tokens. |
|
|
99
|
+
| `pm config list` | Profiles and the environment overrides in effect. |
|
|
100
|
+
|
|
101
|
+
`--json` works before or after the action (`pm --json report list` and `pm report list --json`). Data goes to stdout; messages and errors go to stderr. `pm` exits with `0` on success, `1` when a command fails and `2` when the command line is wrong.
|
|
102
|
+
|
|
103
|
+
### Global options
|
|
104
|
+
|
|
105
|
+
| Option | Meaning |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `--json` | Print JSON instead of tables. |
|
|
108
|
+
| `--quiet` | Suppress informational messages on stderr. Errors still print. |
|
|
109
|
+
| `-v`, `--verbose` | Log each request, its status and timing to stderr; response bodies on errors only. Tokens are never logged. |
|
|
110
|
+
| `--profile <name>` | Use a profile other than `default`. |
|
|
111
|
+
| `--api-url <url>` | Talk to a different PushMetrics address for one command. |
|
|
112
|
+
| `--version` | Print the version and exit. |
|
|
113
|
+
|
|
114
|
+
### Environment variables
|
|
115
|
+
|
|
116
|
+
| Variable | Purpose |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `PM_API_TOKEN` | A personal access token. Wins over a token stored by `pm login --token`. For CI and agents. |
|
|
119
|
+
| `PM_PROFILE` | Profile to use when no `--profile` is given. |
|
|
120
|
+
| `PM_API_URL` | Talk to this address instead of the profile's. |
|
|
121
|
+
| `PM_CONFIG_HOME` | Where configuration and fallback credential files live. Default: `~/.pm`. |
|
|
122
|
+
|
|
123
|
+
## Several workspaces
|
|
124
|
+
|
|
125
|
+
A token works in one workspace. To work in a second one, create a token there and give it its own profile:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pm config set acme api_url https://pushmetrics.io
|
|
129
|
+
pm --profile acme login --token
|
|
130
|
+
pm --profile acme report list
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`pm workspace switch` is for browser login only; with a token, the token decides the workspace.
|
|
134
|
+
|
|
135
|
+
## Where credentials live
|
|
136
|
+
|
|
137
|
+
`pm login --token` stores the token in the OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service) under the service name `pushmetrics-cli-pat`, one entry per profile. Without a usable credential store it falls back to `~/.pm/tokens.json`, mode `0600`.
|
|
138
|
+
|
|
139
|
+
`pm` also caches the short-lived access token it exchanges from your personal access token, under the service name `pushmetrics-cli-jwt` or in `~/.pm/jwt_cache.json` with mode `0600`.
|
|
140
|
+
|
|
141
|
+
`pm logout` removes both for the active profile. It does not revoke the token on the server; use `pm token revoke` or the Access Tokens page for that.
|
|
142
|
+
|
|
143
|
+
`pm` never takes a token on the command line and never prints one, in `-v` output or in errors. It only sends tokens over `https://`, except to `localhost`.
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
**`Not logged in. Run pm login --token with a personal access token, or set PM_API_TOKEN.`** Nothing stored for the active profile and `PM_API_TOKEN` unset. Tokens are stored per profile: check `pm config list`.
|
|
148
|
+
|
|
149
|
+
**`That does not look like a personal access token…`** What was pasted, piped or set in `PM_API_TOKEN` is not a `pmpat_...` value. Copy it again exactly.
|
|
150
|
+
|
|
151
|
+
**`Personal access token rejected (invalid, expired, or revoked).`** Create a new token and log in again.
|
|
152
|
+
|
|
153
|
+
**`Could not reach https://... (ConnectionError)…`** Network, or the wrong address: `pm config list` shows it.
|
|
154
|
+
|
|
155
|
+
**`Unknown profile: 'acme'…`** or **`Profile 'acme' has no api_url…`** Each message ends with the `pm config set` command that fixes it; see also [Several workspaces](#several-workspaces).
|
|
156
|
+
|
|
157
|
+
**`API ... failed: 403` / `404` with a `Hint:` line.** The hint names the next step: permissions in the token's workspace, or an id that is not visible there.
|
|
158
|
+
|
|
159
|
+
**Anything else.** Re-run with `pm -v ...` and look at the response status and body.
|
|
160
|
+
|
|
161
|
+
The [website guide](https://pushmetrics.io/docs/admin/cli/) lists every error with its fix.
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
The CLI lives in the `cli/` directory of the PushMetrics monorepo and must not import anything from the backend packages (`superset/`, `prefect/`); a test enforces this. To work on it:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
cd cli
|
|
169
|
+
python -m venv .venv && source .venv/bin/activate
|
|
170
|
+
pip install -e '.[dev]' # contributors only; users install the released package
|
|
171
|
+
pytest tests/ -v
|
|
172
|
+
ruff check .
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Tests mock HTTP with the `responses` library and never reach a real backend. `tests/repo_only/` holds tests that read files outside `cli/`, such as `test_docs_commands_exist.py`, which checks every `pm` command written in this README and in the website guide against the real command tree; they run from a checkout and are not shipped in the sdist. CI runs the suite on every pull request that touches `cli/`.
|
|
176
|
+
|
|
177
|
+
When you run a real `pm` process during development, set `PM_CONFIG_HOME` to a scratch directory **and** `PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring`. `PM_CONFIG_HOME` alone does not isolate credentials: the OS keyring is keyed by profile name, so a scratch run would read, and `pm logout` would delete, your real stored token.
|
|
178
|
+
|
|
179
|
+
**Adding a resource.** For anything with the standard CRUD shape (`/<resource>/` list, `/<resource>/<uid>` single, plus `POST`/`PUT`/`DELETE`):
|
|
180
|
+
|
|
181
|
+
1. **Model** in `pm/models/<resource>.py`: a Pydantic `BaseModel` with `extra="allow"`; re-export from `pm/models/__init__.py`.
|
|
182
|
+
2. **Client** in `pm/client/<resource>.py`: subclass `CrudClient[Model]`, set `base_path` and `model`. Register it on the `PMClient` facade in `pm/client/__init__.py`.
|
|
183
|
+
3. **Commands** in `pm/commands/<resource>.py`: a `click.group(cls=AliasedGroup, aliases={"ls": "list"})` with `list/get/create/update/delete`; use `parse_data` and `merge_fields` from `pm/commands/_helpers.py` for the `--data '<json>'` pattern. Skip verbs the backend does not support.
|
|
184
|
+
4. **Wire it** in `pm/cli.py` with `cli.add_command(...)`.
|
|
185
|
+
5. **Tests** in `tests/test_client_<resource>.py`, asserting on model attributes.
|
|
186
|
+
6. **Docs**: add the command to the website guide (`website/content/posts/docs/admin/2026-10-12-cli.md`); the docs test fails if a documented command does not exist.
|
|
187
|
+
|
|
188
|
+
Endpoint paths to mirror live in `frontend/src/shared/services/*.ts`; the frontend prefixes everything with `/api/v1`.
|
|
189
|
+
|
|
190
|
+
The `PMClient` facade is reusable from Python without the command layer:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from pm.client import PMClient
|
|
194
|
+
from pm.config import get_profile
|
|
195
|
+
|
|
196
|
+
client = PMClient.from_profile(get_profile("default"))
|
|
197
|
+
for report in client.reports.list():
|
|
198
|
+
print(report.uid, report.name)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Authentication resolution order inside `PMSession.from_profile`: `PM_API_JWT` (a delegation JWT the PushMetrics AI agent sets, used as-is) > personal access token (`PM_API_TOKEN` or stored, exchanged at `/api/v1/security/exchange` for a short-lived workspace-scoped JWT) > Auth0 session cookies from a browser login. Bearer JWTs carry a `workspace_id` claim that the backend installs as the request's workspace, so a pure Bearer client has full workspace context; cookies are the interactive path only.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Click group with command-name aliases + fuzzy "Did you mean?".
|
|
2
|
+
|
|
3
|
+
pm-tool introduces canonical verbs (`list`, `create`, `delete`, `get`) that
|
|
4
|
+
replace older non-REST names (`ls`, `add`, `remove`). The legacy names stay
|
|
5
|
+
as soft aliases — both forms work — so existing CLI users aren't broken.
|
|
6
|
+
Same pattern for the plural→singular group rename (`reports`→`report`).
|
|
7
|
+
|
|
8
|
+
When neither a direct match nor an alias resolves, the group attempts a
|
|
9
|
+
fuzzy suggestion via difflib so typos like ``pm blcok`` get a hint
|
|
10
|
+
toward ``block`` instead of a bare "No such command" error.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
|
|
14
|
+
from pm._alias import AliasedGroup
|
|
15
|
+
|
|
16
|
+
@click.group(name="user", cls=AliasedGroup, aliases={"add": "create"})
|
|
17
|
+
def user():
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@user.command(name="create")
|
|
21
|
+
def create_user(...):
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
Now ``pm user create`` and ``pm user add`` both resolve to ``create_user``.
|
|
25
|
+
Aliases are NOT listed in ``--help`` (to keep the canonical surface clean);
|
|
26
|
+
``pm help`` (pm-foundation) is the agent-facing help system and will
|
|
27
|
+
mention compatibility aliases explicitly when relevant.
|
|
28
|
+
|
|
29
|
+
Groups that don't need legacy aliases still benefit from the fuzzy
|
|
30
|
+
suggestion behavior — pass ``aliases={}`` (the default) and the
|
|
31
|
+
suggestion path is the only added behavior over a plain ``click.Group``.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import difflib
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
import click
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Fuzzy threshold. Codex review round 2 (2026-06-09) showed that 0.7
|
|
42
|
+
# is too tight for 3-char verbs — common transpositions/substitutions
|
|
43
|
+
# like ``gte→get`` and ``rnu→run`` score about 0.67. 0.6 picks those
|
|
44
|
+
# up while still rejecting genuinely-different tokens (``xyz`` vs
|
|
45
|
+
# any verb scores ~0.0). The MIN_LEN floor was also dropped: short
|
|
46
|
+
# tokens like ``ge → get`` (0.8) are HELPFUL suggestions, not noise.
|
|
47
|
+
_FUZZY_CUTOFF = 0.6
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AliasedGroup(click.Group):
|
|
51
|
+
"""Click group that resolves a configurable map of legacy command
|
|
52
|
+
names to their canonical equivalents.
|
|
53
|
+
|
|
54
|
+
Aliases are checked AFTER direct command lookup, so a real command
|
|
55
|
+
with the same name as an alias would always win (defensive — no
|
|
56
|
+
silent shadowing). Unknown commands trigger a fuzzy suggestion
|
|
57
|
+
when one is available; otherwise Click's standard error renders.
|
|
58
|
+
|
|
59
|
+
Subgroups created via ``@aliased_group_instance.group()`` inherit
|
|
60
|
+
this class via the ``group()`` override below — without it, nested
|
|
61
|
+
``@cli.group()`` declarations default back to ``click.Group`` and
|
|
62
|
+
miss the typo behavior. Codex review round 2 catch on
|
|
63
|
+
``cli/pm/cli.py:85`` (``@cli.group(name="config")``).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, *args, aliases: Optional[dict[str, str]] = None, **kwargs):
|
|
67
|
+
super().__init__(*args, **kwargs)
|
|
68
|
+
self._aliases: dict[str, str] = aliases or {}
|
|
69
|
+
|
|
70
|
+
def group(self, *args, **kwargs):
|
|
71
|
+
"""Default subgroup class to ``type(self)`` so nested
|
|
72
|
+
``@aliased_group.group()`` declarations inherit the runtime
|
|
73
|
+
class — transitively, for subclasses of ``AliasedGroup``. Codex
|
|
74
|
+
round 3 catch: hard-coding ``AliasedGroup`` here lost subclass
|
|
75
|
+
behavior on subgroups (a hypothetical ``FancyAliasedGroup``
|
|
76
|
+
would have nested groups fall back to plain ``AliasedGroup``).
|
|
77
|
+
Callers can still override with ``cls=`` if they need a
|
|
78
|
+
different group class for a specific subgroup.
|
|
79
|
+
"""
|
|
80
|
+
kwargs.setdefault("cls", type(self))
|
|
81
|
+
return super().group(*args, **kwargs)
|
|
82
|
+
|
|
83
|
+
def get_command(self, ctx: click.Context, cmd_name: str):
|
|
84
|
+
rv = super().get_command(ctx, cmd_name)
|
|
85
|
+
if rv is not None:
|
|
86
|
+
return rv
|
|
87
|
+
canonical = self._aliases.get(cmd_name)
|
|
88
|
+
if canonical is not None:
|
|
89
|
+
return super().get_command(ctx, canonical)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def resolve_command(self, ctx: click.Context, args):
|
|
93
|
+
"""Run Click's normal resolution; on a missing-command
|
|
94
|
+
``UsageError``, attempt a fuzzy suggestion.
|
|
95
|
+
|
|
96
|
+
Codex round 2 catch: catching every ``UsageError`` over-fires
|
|
97
|
+
on option errors like ``pm --block`` ("No such option"), which
|
|
98
|
+
would mis-render as "Did you mean 'block'?". Filter by the
|
|
99
|
+
``No such command`` prefix Click uses for unknown-command
|
|
100
|
+
errors so option errors fall through to the standard renderer.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
return super().resolve_command(ctx, args)
|
|
104
|
+
except click.UsageError as exc:
|
|
105
|
+
if not _is_no_such_command_error(exc):
|
|
106
|
+
raise
|
|
107
|
+
attempted = args[0] if args else ""
|
|
108
|
+
suggestion = self._fuzzy_suggest(ctx, attempted)
|
|
109
|
+
if suggestion is None:
|
|
110
|
+
raise
|
|
111
|
+
raise click.UsageError(
|
|
112
|
+
f"No such command {attempted!r}. "
|
|
113
|
+
f"Did you mean {suggestion!r}?",
|
|
114
|
+
ctx=ctx,
|
|
115
|
+
) from exc
|
|
116
|
+
|
|
117
|
+
def _fuzzy_suggest(self, ctx: click.Context, attempted: str) -> Optional[str]:
|
|
118
|
+
"""Return the best fuzzy match for ``attempted`` against this
|
|
119
|
+
group's registered commands, or None when no candidate scores
|
|
120
|
+
above the cutoff.
|
|
121
|
+
|
|
122
|
+
Codex round 2 catch: aliases are included in the candidate
|
|
123
|
+
SEARCH set so typos near a legacy alias still resolve, but
|
|
124
|
+
the returned suggestion is the canonical command — never the
|
|
125
|
+
alias. This matches the project's "aliases work but aren't
|
|
126
|
+
advertised in --help" stance.
|
|
127
|
+
|
|
128
|
+
Codex round 3 catch: defensive check that the resolved
|
|
129
|
+
canonical actually exists in ``list_commands``. A malformed
|
|
130
|
+
alias map (``aliases={"ls": "nonexistent"}``) would otherwise
|
|
131
|
+
surface ``"nonexistent"`` as the suggestion — a bad UX on
|
|
132
|
+
top of a config error.
|
|
133
|
+
"""
|
|
134
|
+
if not attempted:
|
|
135
|
+
return None
|
|
136
|
+
registered = set(self.list_commands(ctx))
|
|
137
|
+
candidates = list(registered) + list(self._aliases.keys())
|
|
138
|
+
matches = difflib.get_close_matches(
|
|
139
|
+
attempted, candidates, n=1, cutoff=_FUZZY_CUTOFF,
|
|
140
|
+
)
|
|
141
|
+
if not matches:
|
|
142
|
+
return None
|
|
143
|
+
match = matches[0]
|
|
144
|
+
# Resolve alias → canonical. If ``match`` is a real command,
|
|
145
|
+
# ``self._aliases.get(match)`` returns None and we keep the
|
|
146
|
+
# original — the get() default lookup pattern.
|
|
147
|
+
resolved = self._aliases.get(match, match)
|
|
148
|
+
# Defensive: drop dangling-alias suggestions rather than
|
|
149
|
+
# render a misleading "Did you mean ..." for a command that
|
|
150
|
+
# doesn't exist.
|
|
151
|
+
if resolved not in registered:
|
|
152
|
+
return None
|
|
153
|
+
return resolved
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _is_no_such_command_error(exc: click.UsageError) -> bool:
|
|
157
|
+
"""Click renders unknown-command errors with a message starting
|
|
158
|
+
``"No such command"``. Option errors start ``"No such option"`` and
|
|
159
|
+
other UsageErrors (missing args, invalid values) have unrelated
|
|
160
|
+
prefixes. Filter strictly so the fuzzy suggestion only attaches
|
|
161
|
+
where it makes sense.
|
|
162
|
+
"""
|
|
163
|
+
message = exc.message if exc.message else ""
|
|
164
|
+
return message.lower().startswith("no such command")
|
|
File without changes
|