Metadata-Version: 2.4
Name: pushmetrics-cli
Version: 0.1.0
Summary: Command-line interface for PushMetrics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: click>=8.1
Requires-Dist: requests>=2.31
Requires-Dist: keyring>=24.0
Requires-Dist: pydantic>=2.0
Requires-Dist: tomli>=2.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: setuptools>=68; extra == "dev"
Requires-Dist: wheel; extra == "dev"
Requires-Dist: pyyaml>=6; extra == "dev"

# PushMetrics CLI (`pm`)

`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.

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.

The complete user guide is on the website: [PushMetrics CLI](https://pushmetrics.io/docs/admin/cli/).

## Install

> **PLACEHOLDER (D1).** The install command depends on the distribution channel and is filled in at release time.

```bash
# PLACEHOLDER (D1): install command goes here
pm --version
```

Requires Python 3.9 or newer. Runs on macOS, Linux and Windows.

## Quick start

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.
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:

   ```bash
   pm login --token
   pm whoami
   ```

3. Use it:

   ```bash
   pm report list
   pm report get <report-uid>
   pm report run <report-uid> --send-to-me
   pm run list --last-days 2 --status failed
   pm run logs <run-uid> --level ERROR
   pm report list --json
   ```

In CI, skip `pm login` and set `PM_API_TOKEN` instead.

> **PLACEHOLDER (D2).** Browser login (`pm login` without `--token`, Auth0 device flow) is documented here only if it ships in the first release.

## Commands

`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.

```
Account:
  login   Log in with a personal access token.
  logout  Log out and forget the stored token or session.
  whoami  Show who you are logged in as, and the workspace.
  token   Personal access tokens for API / CLI authentication.
  config  Profiles: which PushMetrics server pm talks to.

Commands:
  block         Blocks of any type.
  chart         Charts (Plotly blocks).
  database      Databases, their schemas and tables.
  email         Email blocks.
  explore       Explore blocks (saved data views).
  favorite      Your favorite reports and other objects.
  folder        Folders that organize reports.
  help          Show compact help for a group, with examples.
  integration   Connected services (Slack, Tableau, Google Drive, ...).
  metrics-view  Metrics views (the semantic layer).
  organization  Your organization (the owner of workspaces).
  recipient     Recipients: who reports are delivered to.
  report        Reports and their runs.
  role          Roles and groups in the current workspace.
  run           Report runs: status, results and logs.
  saved-block   Saved blocks (reusable block templates).
  sftp          SFTP upload blocks.
  slack         Slack message blocks.
  slides        Google Slides blocks.
  sql           SQL query blocks.
  tableau       Tableau view blocks.
  user          Users in the current workspace.
  webhook       Webhooks that trigger report runs.
  workspace     Workspaces you belong to.
```

The ones most people need:

| Command | What it does |
|---|---|
| `pm whoami` | Your user and the token's workspace. |
| `pm report list` | Reports in the workspace. `--name` filters by name, `--limit` caps the rows. |
| `pm report get <uid>` | One report. Full record with `--json`. |
| `pm report yaml <uid>` | The report's full YAML configuration, to stdout. |
| `pm report apply <file> --uid <uid>` | Write a YAML file back to a report. `--dry-run` shows the diff without saving. |
| `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. |
| `pm run list` | Recent runs, newest first. `--report <uid>`, `--last-days N`, `--status failed`, `--limit N`. |
| `pm run get <run-uid>` | A run's status and timing. |
| `pm run logs <run-uid>` | A run's log lines. `--level ERROR` keeps only errors. |
| `pm run results <run-uid>` | What each block of the run produced. |
| `pm token list` / `create <name>` / `revoke <id>` | Manage your personal access tokens. |
| `pm config list` | Profiles and the environment overrides in effect. |

`--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.

### Global options

| Option | Meaning |
|---|---|
| `--json` | Print JSON instead of tables. |
| `--quiet` | Suppress informational messages on stderr. Errors still print. |
| `-v`, `--verbose` | Log each request, its status and timing to stderr; response bodies on errors only. Tokens are never logged. |
| `--profile <name>` | Use a profile other than `default`. |
| `--api-url <url>` | Talk to a different PushMetrics address for one command. |
| `--version` | Print the version and exit. |

### Environment variables

| Variable | Purpose |
|---|---|
| `PM_API_TOKEN` | A personal access token. Wins over a token stored by `pm login --token`. For CI and agents. |
| `PM_PROFILE` | Profile to use when no `--profile` is given. |
| `PM_API_URL` | Talk to this address instead of the profile's. |
| `PM_CONFIG_HOME` | Where configuration and fallback credential files live. Default: `~/.pm`. |

## Several workspaces

A token works in one workspace. To work in a second one, create a token there and give it its own profile:

```bash
pm config set acme api_url https://pushmetrics.io
pm --profile acme login --token
pm --profile acme report list
```

`pm workspace switch` is for browser login only; with a token, the token decides the workspace.

## Where credentials live

`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`.

`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`.

`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.

`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`.

## Troubleshooting

**`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`.

**`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.

**`Personal access token rejected (invalid, expired, or revoked).`** Create a new token and log in again.

**`Could not reach https://... (ConnectionError)…`** Network, or the wrong address: `pm config list` shows it.

**`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).

**`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.

**Anything else.** Re-run with `pm -v ...` and look at the response status and body.

The [website guide](https://pushmetrics.io/docs/admin/cli/) lists every error with its fix.

## Contributing

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:

```bash
cd cli
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'          # contributors only; users install the released package
pytest tests/ -v
ruff check .
```

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/`.

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.

**Adding a resource.** For anything with the standard CRUD shape (`/<resource>/` list, `/<resource>/<uid>` single, plus `POST`/`PUT`/`DELETE`):

1. **Model** in `pm/models/<resource>.py`: a Pydantic `BaseModel` with `extra="allow"`; re-export from `pm/models/__init__.py`.
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`.
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.
4. **Wire it** in `pm/cli.py` with `cli.add_command(...)`.
5. **Tests** in `tests/test_client_<resource>.py`, asserting on model attributes.
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.

Endpoint paths to mirror live in `frontend/src/shared/services/*.ts`; the frontend prefixes everything with `/api/v1`.

The `PMClient` facade is reusable from Python without the command layer:

```python
from pm.client import PMClient
from pm.config import get_profile

client = PMClient.from_profile(get_profile("default"))
for report in client.reports.list():
    print(report.uid, report.name)
```

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.
