pushmetrics-cli 0.1.0__py3-none-any.whl
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.
- pm/__init__.py +1 -0
- pm/__main__.py +4 -0
- pm/_alias.py +164 -0
- pm/auth/__init__.py +0 -0
- pm/auth/device_flow.py +187 -0
- pm/auth/exchange.py +79 -0
- pm/auth/jwt_cache.py +163 -0
- pm/auth/secret_files.py +58 -0
- pm/auth/session.py +440 -0
- pm/auth/store.py +160 -0
- pm/auth/token.py +289 -0
- pm/cli.py +282 -0
- pm/client/__init__.py +68 -0
- pm/client/access_tokens.py +54 -0
- pm/client/base.py +229 -0
- pm/client/blocks.py +461 -0
- pm/client/databases.py +147 -0
- pm/client/favorites.py +14 -0
- pm/client/folders.py +10 -0
- pm/client/integrations.py +14 -0
- pm/client/organizations.py +20 -0
- pm/client/recipients.py +92 -0
- pm/client/reports.py +148 -0
- pm/client/roles.py +100 -0
- pm/client/runs.py +134 -0
- pm/client/saved_blocks.py +13 -0
- pm/client/tableau.py +108 -0
- pm/client/users.py +71 -0
- pm/client/webhooks.py +43 -0
- pm/client/workspaces.py +34 -0
- pm/commands/__init__.py +0 -0
- pm/commands/_block_factory.py +762 -0
- pm/commands/_helpers.py +178 -0
- pm/commands/auth.py +222 -0
- pm/commands/block.py +26 -0
- pm/commands/chart.py +20 -0
- pm/commands/db.py +291 -0
- pm/commands/email.py +13 -0
- pm/commands/explore.py +20 -0
- pm/commands/favorites.py +77 -0
- pm/commands/folders.py +113 -0
- pm/commands/help.py +643 -0
- pm/commands/integrations.py +95 -0
- pm/commands/metrics_view.py +195 -0
- pm/commands/organizations.py +56 -0
- pm/commands/recipient.py +226 -0
- pm/commands/reports.py +364 -0
- pm/commands/roles.py +102 -0
- pm/commands/runs.py +214 -0
- pm/commands/saved_blocks.py +97 -0
- pm/commands/sftp.py +19 -0
- pm/commands/slack.py +13 -0
- pm/commands/slides.py +216 -0
- pm/commands/sql.py +17 -0
- pm/commands/tableau.py +148 -0
- pm/commands/token.py +107 -0
- pm/commands/users.py +262 -0
- pm/commands/webhooks.py +132 -0
- pm/commands/workspace.py +156 -0
- pm/config.py +192 -0
- pm/errors.py +228 -0
- pm/log.py +63 -0
- pm/models/__init__.py +31 -0
- pm/models/access_tokens.py +25 -0
- pm/models/blocks.py +21 -0
- pm/models/databases.py +15 -0
- pm/models/favorites.py +14 -0
- pm/models/folders.py +14 -0
- pm/models/integrations.py +20 -0
- pm/models/organizations.py +13 -0
- pm/models/recipients.py +41 -0
- pm/models/reports.py +16 -0
- pm/models/roles.py +22 -0
- pm/models/runs.py +56 -0
- pm/models/saved_blocks.py +15 -0
- pm/models/users.py +36 -0
- pm/models/webhooks.py +23 -0
- pm/models/workspaces.py +16 -0
- pm/output.py +92 -0
- pm/types.py +130 -0
- pm/validators.py +499 -0
- pushmetrics_cli-0.1.0.dist-info/METADATA +221 -0
- pushmetrics_cli-0.1.0.dist-info/RECORD +86 -0
- pushmetrics_cli-0.1.0.dist-info/WHEEL +5 -0
- pushmetrics_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pushmetrics_cli-0.1.0.dist-info/top_level.txt +1 -0
pm/commands/help.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"""`pm help` — terse, agent-tuned help pages (pm-tool plan §2.3 / tasks A.1).
|
|
2
|
+
|
|
3
|
+
Click already auto-renders verbose help via ``pm --help`` / ``pm <group>
|
|
4
|
+
--help`` — that's the human-facing fallback. ``pm help`` is the
|
|
5
|
+
*agent-facing* surface: every page is ≤600 chars (vs Click's ~1500–3000
|
|
6
|
+
char rendering), structured as "Verbs → Body fields → Example", and
|
|
7
|
+
filtered by the ``PM_AGENT_DENIED_ACTIONS`` env var so denied actions
|
|
8
|
+
don't even appear in help (defense in depth alongside dispatch-layer
|
|
9
|
+
denial in pm_tool.py per pm-consolidation).
|
|
10
|
+
|
|
11
|
+
Why short pages matter
|
|
12
|
+
----------------------
|
|
13
|
+
The agent burns input tokens on every help fetch. A 3000-char Click
|
|
14
|
+
``--help`` rendering pushes ~750 tokens through every time. A 500-char
|
|
15
|
+
``pm help`` page is ~125 tokens. At the scale of hundreds of agent
|
|
16
|
+
help fetches per workspace per day, the delta compounds.
|
|
17
|
+
|
|
18
|
+
Suppression contract
|
|
19
|
+
--------------------
|
|
20
|
+
``PM_AGENT_DENIED_ACTIONS`` (CSV of ``<noun>.<verb>`` canonical_actions)
|
|
21
|
+
is the single policy source. ``pm_tool.py`` reads it for dispatch
|
|
22
|
+
denial; this module reads it for help rendering. Setting it to
|
|
23
|
+
``block.delete,sftp.create,sftp.send`` (the default ``single_tool`` list)
|
|
24
|
+
hides those verbs from ``pm help block`` and ``pm help sftp``.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
from typing import Dict, List, Optional
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── Group-name → backend noun (for canonical_action mapping) ─────────
|
|
35
|
+
#
|
|
36
|
+
# Duplicates the map in prefect/queryme/agent/canonical_actions.py
|
|
37
|
+
# because the CLI is HTTP-only (cli/tests/test_no_backend_imports.py)
|
|
38
|
+
# and can't pull from the prefect agent. Keep these two maps in sync
|
|
39
|
+
# manually — small static data, low churn risk.
|
|
40
|
+
|
|
41
|
+
_GROUP_TO_NOUN: Dict[str, str] = {
|
|
42
|
+
"sql": "sql",
|
|
43
|
+
"email": "email",
|
|
44
|
+
"slack": "slack",
|
|
45
|
+
"sftp": "sftp",
|
|
46
|
+
"chart": "plotly",
|
|
47
|
+
"explore": "explore",
|
|
48
|
+
"metrics-view": "metrics_view",
|
|
49
|
+
"tableau": "tableau",
|
|
50
|
+
"slides": "google_slides",
|
|
51
|
+
"block": "block",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Per-group pages ──────────────────────────────────────────────────
|
|
56
|
+
#
|
|
57
|
+
# Each entry has `verbs` (ordered dict mapping CLI verb → one-line
|
|
58
|
+
# description) plus a `page` template that includes verb list + body
|
|
59
|
+
# fields + example. Verbs are filtered against PM_AGENT_DENIED_ACTIONS
|
|
60
|
+
# before rendering — the page template gets the filtered verb dict
|
|
61
|
+
# back.
|
|
62
|
+
|
|
63
|
+
_GROUP_PAGES: Dict[str, Dict] = {
|
|
64
|
+
"sql": {
|
|
65
|
+
"verbs": {
|
|
66
|
+
"create": "Create a SQL block (returns uid; does NOT run).",
|
|
67
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
68
|
+
"get": "Show one. <uid>.",
|
|
69
|
+
"run": "Execute by <uid>, or transient override via --stdin against an assigned scratch (no persist).",
|
|
70
|
+
"results": "Past execution result(s). <uid> [--last|--result-id].",
|
|
71
|
+
},
|
|
72
|
+
"body_fields": (
|
|
73
|
+
" sql (req) SQL query string\n"
|
|
74
|
+
" database_id (req) UID of the database to run against\n"
|
|
75
|
+
" limit (opt) row limit"
|
|
76
|
+
),
|
|
77
|
+
"example": (
|
|
78
|
+
" echo '{\"sql\":\"SELECT 1\",\"database_id\":\"<db_uid>\"}' \\\n"
|
|
79
|
+
" | pm sql run --stdin --json"
|
|
80
|
+
),
|
|
81
|
+
"summary": "Create + run SQL blocks (block_type=sql).",
|
|
82
|
+
},
|
|
83
|
+
"email": {
|
|
84
|
+
"verbs": {
|
|
85
|
+
"create": "Create; does not send.",
|
|
86
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
87
|
+
"get": "Show one. <uid>.",
|
|
88
|
+
"send": "Send <uid>, or a --stdin body once.",
|
|
89
|
+
"results": "Past sends. <uid> [--last|--result-id].",
|
|
90
|
+
},
|
|
91
|
+
"body_fields": (
|
|
92
|
+
" subject (req)\n"
|
|
93
|
+
" body (req) HTML/text\n"
|
|
94
|
+
" integration_id (req) email integration uid\n"
|
|
95
|
+
" to_ (req) string or list — note trailing _\n"
|
|
96
|
+
" cc, bcc (opt) string or list"
|
|
97
|
+
),
|
|
98
|
+
"example": (
|
|
99
|
+
" echo '{\"subject\":\"Hi\",\"body\":\"x\",\"integration_id\":\"<int>\",\"to_\":\"a@b.com\"}' \\\n"
|
|
100
|
+
" | pm email send --stdin --json"
|
|
101
|
+
),
|
|
102
|
+
"summary": "Create + send email blocks.",
|
|
103
|
+
},
|
|
104
|
+
"slack": {
|
|
105
|
+
"verbs": {
|
|
106
|
+
"create": "Create a slack block.",
|
|
107
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
108
|
+
"get": "Show one. <uid>.",
|
|
109
|
+
"send": "Send by <uid>, or transient override via --stdin against an assigned scratch (no persist).",
|
|
110
|
+
"results": "Past send result(s).",
|
|
111
|
+
},
|
|
112
|
+
"body_fields": (
|
|
113
|
+
" integration_id (req) slack integration uid\n"
|
|
114
|
+
" channels (req) LIST of channel names or DM ids\n"
|
|
115
|
+
" message (req)"
|
|
116
|
+
),
|
|
117
|
+
"example": (
|
|
118
|
+
" echo '{\"integration_id\":\"<int>\",\"channels\":[\"#alerts\"],\"message\":\"deploy ok\"}' \\\n"
|
|
119
|
+
" | pm slack send --stdin --json"
|
|
120
|
+
),
|
|
121
|
+
"summary": "Create + send Slack message blocks.",
|
|
122
|
+
},
|
|
123
|
+
"sftp": {
|
|
124
|
+
"verbs": {
|
|
125
|
+
"create": "Create an SFTP block.",
|
|
126
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
127
|
+
"get": "Show one. <uid>.",
|
|
128
|
+
"send": "Upload by <uid>, or transient override via --stdin against an assigned scratch (no persist).",
|
|
129
|
+
"results": "Past upload result(s).",
|
|
130
|
+
},
|
|
131
|
+
"body_fields": (
|
|
132
|
+
" integration_id (req)\n"
|
|
133
|
+
" path (req) destination path on the SFTP server\n"
|
|
134
|
+
" attachments (opt) list of URLs/refs (e.g. csv_download_url\n"
|
|
135
|
+
" from prior run_sql) to upload"
|
|
136
|
+
),
|
|
137
|
+
"example": (
|
|
138
|
+
" pm sftp send <uid> --json"
|
|
139
|
+
),
|
|
140
|
+
"summary": "Create + send SFTP upload blocks.",
|
|
141
|
+
},
|
|
142
|
+
"chart": {
|
|
143
|
+
"verbs": {
|
|
144
|
+
"create": "Create + auto-render. --name=<n> --stdin.",
|
|
145
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
146
|
+
"get": "Show one. <uid>.",
|
|
147
|
+
"update": "<uid> --stdin (replaces the Plotly spec).",
|
|
148
|
+
"run": "Re-render an existing chart block. <uid>.",
|
|
149
|
+
"results": "Past render result(s).",
|
|
150
|
+
},
|
|
151
|
+
"body_fields": (
|
|
152
|
+
" properties (req) {data, layout} per Plotly schema\n"
|
|
153
|
+
" image_filename (opt)"
|
|
154
|
+
),
|
|
155
|
+
"example": (
|
|
156
|
+
" pm chart create --name=q4-rev --stdin < spec.json"
|
|
157
|
+
),
|
|
158
|
+
"summary": (
|
|
159
|
+
"Create + render Plotly chart blocks (block_type=plotly).\n"
|
|
160
|
+
"NOTE: creating a chart also renders the PNG as a side effect."
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
"explore": {
|
|
164
|
+
"verbs": {
|
|
165
|
+
"create": "Create an explore block. --name=<n> --stdin.",
|
|
166
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
167
|
+
"get": "Show one. <uid>.",
|
|
168
|
+
"update": "<uid> --stdin (replaces the saved-view definition).",
|
|
169
|
+
},
|
|
170
|
+
"body_fields": " (free-form — saved-view definition)",
|
|
171
|
+
"example": (
|
|
172
|
+
" pm explore create --name=daily-funnel --stdin < view.json\n"
|
|
173
|
+
" pm explore list --json"
|
|
174
|
+
),
|
|
175
|
+
"summary": (
|
|
176
|
+
"Create explore (saved-view) blocks.\n"
|
|
177
|
+
"NOTE: no execute verb — the UI renders these."
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
"metrics-view": {
|
|
181
|
+
"verbs": {
|
|
182
|
+
"create": "Create.",
|
|
183
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
184
|
+
"get": "Show one. <uid>.",
|
|
185
|
+
"update": "<uid> --stdin.",
|
|
186
|
+
"query": "Semantic query. <uid> [flags or --stdin].",
|
|
187
|
+
"schema": "Available measures/dimensions. <uid>.",
|
|
188
|
+
},
|
|
189
|
+
"body_fields": (
|
|
190
|
+
" --measure NAME repeatable, ≥1 required\n"
|
|
191
|
+
" --dimension NAME repeatable\n"
|
|
192
|
+
" --time-range last_30d | 2026-01-01:2026-04-01\n"
|
|
193
|
+
" --limit N, --offset N, --no-cache\n"
|
|
194
|
+
" --stdin for filters/order_by/parameters"
|
|
195
|
+
),
|
|
196
|
+
"example": (
|
|
197
|
+
" pm metrics-view query <uid> --measure=orders --dimension=region --json"
|
|
198
|
+
),
|
|
199
|
+
"summary": "Manage + query metrics view blocks (semantic layer).",
|
|
200
|
+
},
|
|
201
|
+
"tableau": {
|
|
202
|
+
"verbs": {
|
|
203
|
+
"create": "Create.",
|
|
204
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
205
|
+
"get": "Show one. <uid>.",
|
|
206
|
+
"run": "Exports/downloads Tableau view as PNG/PDF/embed URL.",
|
|
207
|
+
"results": "Past render result(s).",
|
|
208
|
+
"search": "List views (or --workbooks).",
|
|
209
|
+
"view": "One view's metadata.",
|
|
210
|
+
},
|
|
211
|
+
"body_fields": (
|
|
212
|
+
" --integration <uid> search/view integration\n"
|
|
213
|
+
" --keyword <text> name filter\n"
|
|
214
|
+
" --workbook-id <id> narrow search\n"
|
|
215
|
+
" --workbooks search workbooks\n"
|
|
216
|
+
" --view-id <id> required for view"
|
|
217
|
+
),
|
|
218
|
+
"example": (
|
|
219
|
+
" pm tableau search --integration=<uid> --keyword=Sales --json"
|
|
220
|
+
),
|
|
221
|
+
"summary": "Tableau blocks + discovery.",
|
|
222
|
+
},
|
|
223
|
+
"slides": {
|
|
224
|
+
"verbs": {
|
|
225
|
+
"create": "Create.",
|
|
226
|
+
"list": "List. [--name <text>] [--limit <n>].",
|
|
227
|
+
"get": "Show one. <uid>.",
|
|
228
|
+
"run": "Compose presentation.",
|
|
229
|
+
"results": "Past render result(s).",
|
|
230
|
+
"parse-template": "Parse master deck (once after create).",
|
|
231
|
+
"export": "Export as PDF. [--filename]",
|
|
232
|
+
"batch-update": "Slides API batchUpdate. --stdin.",
|
|
233
|
+
},
|
|
234
|
+
"body_fields": (
|
|
235
|
+
" --stdin JSON:\n"
|
|
236
|
+
" integration_id, template_id, source_file_id"
|
|
237
|
+
),
|
|
238
|
+
"example": (
|
|
239
|
+
" pm slides parse-template <uid> --json\n"
|
|
240
|
+
" pm slides export <uid> --filename=q4.pdf --json"
|
|
241
|
+
),
|
|
242
|
+
"summary": (
|
|
243
|
+
"Google Slides blocks (google_slides)."
|
|
244
|
+
),
|
|
245
|
+
},
|
|
246
|
+
"block": {
|
|
247
|
+
"verbs": {
|
|
248
|
+
"get": "Show one. <uid>.",
|
|
249
|
+
"list": "List. [--type=<T>] [--name <text>] [--limit <n>].",
|
|
250
|
+
"create": "--type=<T> --name=<n> --stdin.",
|
|
251
|
+
"update": "<uid> [--name=<n>] [--stdin].",
|
|
252
|
+
"delete": "<uid> [--yes].",
|
|
253
|
+
"run": "Execute (stored type wins; --type rejected).",
|
|
254
|
+
"results": "<uid> [--last|--result-id].",
|
|
255
|
+
},
|
|
256
|
+
"body_fields": (
|
|
257
|
+
" content_json shape on create depends on --type\n"
|
|
258
|
+
" (see `pm help <type>` for per-type fields)."
|
|
259
|
+
),
|
|
260
|
+
"example": (
|
|
261
|
+
" pm block list --type=sql --json\n"
|
|
262
|
+
" pm block create --type=email --name=daily --stdin < body.json"
|
|
263
|
+
),
|
|
264
|
+
"summary": "CRUD plumbing across all block types.",
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
# ── CRUD resource groups ──────────────────────────────────────────
|
|
268
|
+
# Pages for the non-typed-block-group verbs. Without these, ``pm
|
|
269
|
+
# help report`` (etc.) returned ``No help for 'report'``, leaving
|
|
270
|
+
# the agent to guess flags after every failure (session N8wE87kOmz
|
|
271
|
+
# took 46 tool calls partly because of this discoverability gap).
|
|
272
|
+
|
|
273
|
+
"report": {
|
|
274
|
+
"verbs": {
|
|
275
|
+
"list": "List reports (newest first). [--name <text>] [--limit <n>].",
|
|
276
|
+
"get": "Show one. <uid>.",
|
|
277
|
+
"yaml": "Dump YAML. <uid>.",
|
|
278
|
+
"update": "<uid> [--name=<n>].",
|
|
279
|
+
"apply": (
|
|
280
|
+
"YAML via stdin: `pm report apply -`. With --uid, blocks "
|
|
281
|
+
"missing from the YAML are DELETED — use --no-prune to "
|
|
282
|
+
"only upsert, --dry-run to preview."
|
|
283
|
+
),
|
|
284
|
+
"run": "Trigger NEW run. For past runs use `pm run list --report=<uid>`.",
|
|
285
|
+
"delete": "<uid> [--yes].",
|
|
286
|
+
},
|
|
287
|
+
"example": (
|
|
288
|
+
" pm report list --json\n"
|
|
289
|
+
" pm run list --report=<uid> --last-days=7\n"
|
|
290
|
+
" pm report apply - --uid=<uid> # YAML via tool's stdin"
|
|
291
|
+
),
|
|
292
|
+
"summary": "Reports and their runs.",
|
|
293
|
+
},
|
|
294
|
+
"run": {
|
|
295
|
+
"verbs": {
|
|
296
|
+
"list": "Recent runs. [--report <uid>] [--last-days <n>] [--status <s>] [--limit <n>].",
|
|
297
|
+
"get": "Status, timing, flow_run_id. <run_uid>.",
|
|
298
|
+
"results": "Per-block results for a run. <run_uid>.",
|
|
299
|
+
"logs": "Log lines for a run. <run_uid|flow_run_id>.",
|
|
300
|
+
},
|
|
301
|
+
"body_fields": (
|
|
302
|
+
" ``get`` is metadata only — NO row data (use `pm sql run\n"
|
|
303
|
+
" --stdin`). Debug a failed report: list its runs, then\n"
|
|
304
|
+
" read one run's logs (`--level=ERROR` for failure lines)."
|
|
305
|
+
),
|
|
306
|
+
"example": (
|
|
307
|
+
" pm run list --report=<report_uid> --last-days=2 --status=failed\n"
|
|
308
|
+
" pm run logs <run_uid> --level=ERROR --json"
|
|
309
|
+
),
|
|
310
|
+
"summary": "Noteflow run inspection.",
|
|
311
|
+
},
|
|
312
|
+
"database": {
|
|
313
|
+
"verbs": {
|
|
314
|
+
"list": "Workspace databases. [--name <text>] [--limit <n>].",
|
|
315
|
+
"schemas": "<db_ref>.",
|
|
316
|
+
"tables": "<db_ref> <schema>.",
|
|
317
|
+
"describe": "<db_ref> <schema> <table>.",
|
|
318
|
+
"select-star": "SELECT * SQL, not run. <db_ref> <schema> <table>.",
|
|
319
|
+
"exists": "<db_ref> <schema> <table>.",
|
|
320
|
+
"search": "<db_ref> <pattern> [--schema <s>].",
|
|
321
|
+
"functions": "<db_ref>.",
|
|
322
|
+
},
|
|
323
|
+
"body_fields": (
|
|
324
|
+
" <db_ref>: uid or name (quote spaces).\n"
|
|
325
|
+
" select-star prints SQL; pipe it into `pm sql run --stdin`."
|
|
326
|
+
),
|
|
327
|
+
"example": (
|
|
328
|
+
" pm database list --json\n"
|
|
329
|
+
" pm database tables \"Demo DB\" public --json"
|
|
330
|
+
),
|
|
331
|
+
"summary": "Database introspection.",
|
|
332
|
+
},
|
|
333
|
+
"folder": {
|
|
334
|
+
"verbs": {
|
|
335
|
+
"list": "Workspace folders. [--name <text>] [--limit <n>].",
|
|
336
|
+
"get": "Show one folder. <uid>.",
|
|
337
|
+
"create": "Create a folder. --name=<n> [--parent=<uid>].",
|
|
338
|
+
"update": "Rename / move. <uid> [--name=<n>] [--parent=<uid>].",
|
|
339
|
+
"delete": "Delete a folder. <uid> [--yes].",
|
|
340
|
+
},
|
|
341
|
+
"example": (
|
|
342
|
+
" pm folder list --json\n"
|
|
343
|
+
" pm folder create --name=Marketing --json"
|
|
344
|
+
),
|
|
345
|
+
"summary": "Folder CRUD.",
|
|
346
|
+
},
|
|
347
|
+
"user": {
|
|
348
|
+
"verbs": {
|
|
349
|
+
"list": "Workspace users. [--name <text>] [--limit <n>].",
|
|
350
|
+
"get": "Show one user. <uid>.",
|
|
351
|
+
"create": "Create a user. --email=<e> [--name=<n>].",
|
|
352
|
+
"delete": "Delete a user. <uid> [--yes].",
|
|
353
|
+
"set-role": "Assign a role. <user_uid> <role_name>.",
|
|
354
|
+
},
|
|
355
|
+
"example": (
|
|
356
|
+
" pm user list --json\n"
|
|
357
|
+
" pm user set-role <user_uid> editor --json"
|
|
358
|
+
),
|
|
359
|
+
"summary": "User management.",
|
|
360
|
+
},
|
|
361
|
+
"recipient": {
|
|
362
|
+
"verbs": {
|
|
363
|
+
"list": "Recipients in the workspace CRM. [--name <text>] [--limit <n>].",
|
|
364
|
+
"get": "Show one recipient. <uid>.",
|
|
365
|
+
"details": "Aggregate read (linked destinations etc.). <uid>.",
|
|
366
|
+
"create": "Create a recipient. --name=<n> --email=<e>.",
|
|
367
|
+
"update": "Update recipient. <uid> [--name=<n>] [--email=<e>].",
|
|
368
|
+
"delete": "Delete recipient. <uid> [--yes].",
|
|
369
|
+
},
|
|
370
|
+
"example": (
|
|
371
|
+
" pm recipient list --json\n"
|
|
372
|
+
" pm recipient create --name=\"Alice\" --email=alice@acme.test --json"
|
|
373
|
+
),
|
|
374
|
+
"summary": "Recipients (delivery-target CRM).",
|
|
375
|
+
},
|
|
376
|
+
"integration": {
|
|
377
|
+
"verbs": {
|
|
378
|
+
"list": "Workspace integrations (Tableau / Slack / Email / SFTP / GCP / S3 / Slides etc.). [--name <text>] [--type <t>] [--limit <n>].",
|
|
379
|
+
},
|
|
380
|
+
"body_fields": (
|
|
381
|
+
" Use ``--type=tableau`` (etc.) to filter to one type.\n"
|
|
382
|
+
" Pass the resulting uid as ``--integration=<uid>`` to\n"
|
|
383
|
+
" search / view / send verbs."
|
|
384
|
+
),
|
|
385
|
+
"example": (
|
|
386
|
+
" pm integration list --json\n"
|
|
387
|
+
" pm integration list --type=tableau --json"
|
|
388
|
+
),
|
|
389
|
+
"summary": "Integration discovery.",
|
|
390
|
+
},
|
|
391
|
+
"favorite": {
|
|
392
|
+
"verbs": {
|
|
393
|
+
"list": "Favorited blocks / reports. [--limit <n>].",
|
|
394
|
+
"create": "Mark as favorite. <uid>.",
|
|
395
|
+
"delete": "Unfavorite. <uid>.",
|
|
396
|
+
},
|
|
397
|
+
"example": (
|
|
398
|
+
" pm favorite list --json"
|
|
399
|
+
),
|
|
400
|
+
"summary": "Favorites management.",
|
|
401
|
+
},
|
|
402
|
+
"organization": {
|
|
403
|
+
"verbs": {
|
|
404
|
+
"current": "Show the calling user's organization.",
|
|
405
|
+
},
|
|
406
|
+
"example": (
|
|
407
|
+
" pm organization current --json"
|
|
408
|
+
),
|
|
409
|
+
"summary": "Organization discovery.",
|
|
410
|
+
},
|
|
411
|
+
"role": {
|
|
412
|
+
"verbs": {
|
|
413
|
+
"list": "Roles available in the workspace. [--name <text>] [--limit <n>].",
|
|
414
|
+
"assign": "Assign role to user. <user_uid> <role_name>.",
|
|
415
|
+
"revoke": "Revoke role from user. <user_uid> <role_name>.",
|
|
416
|
+
},
|
|
417
|
+
"example": (
|
|
418
|
+
" pm role list --json\n"
|
|
419
|
+
" pm role assign <user_uid> editor --json"
|
|
420
|
+
),
|
|
421
|
+
"summary": "Role assignments.",
|
|
422
|
+
},
|
|
423
|
+
"saved-block": {
|
|
424
|
+
"verbs": {
|
|
425
|
+
"list": "Saved (template) blocks in the workspace. [--name <text>] [--limit <n>].",
|
|
426
|
+
"update": "Update a saved block. <uid> [--name=<n>] [--stdin].",
|
|
427
|
+
"delete": "Delete a saved block. <uid> [--yes].",
|
|
428
|
+
},
|
|
429
|
+
"example": (
|
|
430
|
+
" pm saved-block list --json"
|
|
431
|
+
),
|
|
432
|
+
"summary": "Saved-block (template) management.",
|
|
433
|
+
},
|
|
434
|
+
"workspace": {
|
|
435
|
+
"verbs": {
|
|
436
|
+
"list": "Workspaces this user belongs to. [--name <text>] [--limit <n>].",
|
|
437
|
+
"current": "Show the active workspace for this session.",
|
|
438
|
+
},
|
|
439
|
+
"body_fields": (
|
|
440
|
+
" Switching / creating / deleting workspaces is intentionally\n"
|
|
441
|
+
" NOT available to the agent — context comes from the calling\n"
|
|
442
|
+
" user's session."
|
|
443
|
+
),
|
|
444
|
+
"example": (
|
|
445
|
+
" pm workspace current --json\n"
|
|
446
|
+
" pm workspace list --json"
|
|
447
|
+
),
|
|
448
|
+
"summary": "Workspace discovery (read-only for agents).",
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ── Account topics (plain pages, no verb table) ──────────────────────
|
|
454
|
+
#
|
|
455
|
+
# Listed on the top page, so `pm help <topic>` must answer for each.
|
|
456
|
+
|
|
457
|
+
_PLAIN_PAGES: Dict[str, str] = {
|
|
458
|
+
"login": (
|
|
459
|
+
"pm login — Log in with a personal access token.\n\n"
|
|
460
|
+
" pm login --token paste the token at the hidden prompt\n"
|
|
461
|
+
" <secrets-cmd> | pm login --token pipe it from a secrets manager\n"
|
|
462
|
+
" PM_API_TOKEN=... use it without storing (CI, scripts)\n\n"
|
|
463
|
+
"Tokens are created in the web app under Access Tokens."
|
|
464
|
+
),
|
|
465
|
+
"logout": (
|
|
466
|
+
"pm logout — Forget the stored token or session for the profile.\n\n"
|
|
467
|
+
" pm logout\n pm --profile staging logout"
|
|
468
|
+
),
|
|
469
|
+
"whoami": (
|
|
470
|
+
"pm whoami — Show who you are logged in as, and the workspace.\n\n"
|
|
471
|
+
" pm whoami --json\n\n"
|
|
472
|
+
"A token is bound to the workspace it was created in."
|
|
473
|
+
),
|
|
474
|
+
"token": (
|
|
475
|
+
"pm token — Personal access tokens.\n\n"
|
|
476
|
+
"Verbs:\n"
|
|
477
|
+
" list Your tokens (metadata only). [--name <text>] [--limit <n>]\n"
|
|
478
|
+
" create Create one; the value is printed once, on stdout.\n"
|
|
479
|
+
" revoke Revoke by numeric ID. <id>\n\n"
|
|
480
|
+
"Example:\n pm token create ci-exports --expires-on 2027-01-01"
|
|
481
|
+
),
|
|
482
|
+
"config": (
|
|
483
|
+
"pm config — Profiles: which PushMetrics server pm talks to.\n\n"
|
|
484
|
+
"Verbs:\n"
|
|
485
|
+
" list Profiles and active env overrides.\n"
|
|
486
|
+
" set Set one field. <profile> <key> <value>\n"
|
|
487
|
+
" keys: api_url auth0_domain auth0_client_id auth0_audience\n\n"
|
|
488
|
+
"Example:\n pm config set staging api_url https://staging.example.com"
|
|
489
|
+
),
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ── Top-level page (≤800 chars) ──────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# NOTE: the surface map below only lists groups + verbs the agent can
|
|
497
|
+
# actually invoke via the cli_adapter ALLOWED_COMMANDS set. `auth`
|
|
498
|
+
# (login / logout / whoami), `webhook`, and the mutating
|
|
499
|
+
# `workspace` / `organization` verbs are deliberately omitted —
|
|
500
|
+
# the agent runs under a delegation JWT (no Auth0 needed), has no
|
|
501
|
+
# webhook surface allowed, and inherits its workspace + organization
|
|
502
|
+
# context from the calling user's session.
|
|
503
|
+
_TOP_LEVEL_PAGE = """pm — PushMetrics CLI.
|
|
504
|
+
|
|
505
|
+
Common:
|
|
506
|
+
pm sql run --stdin --json Run SQL via assigned scratch (body on stdin)
|
|
507
|
+
pm report list List reports
|
|
508
|
+
pm block list --type=sql Filter blocks by type
|
|
509
|
+
pm metrics-view query <uid> Run a semantic query
|
|
510
|
+
pm tableau search --integration=<uid> List Tableau views
|
|
511
|
+
|
|
512
|
+
Surface:
|
|
513
|
+
Domain actions: sql email slack sftp chart explore metrics-view
|
|
514
|
+
tableau slides
|
|
515
|
+
Resources: report folder favorite user role organization
|
|
516
|
+
saved-block integration recipient run
|
|
517
|
+
Block plumbing: block
|
|
518
|
+
DB: database
|
|
519
|
+
Workspace: workspace
|
|
520
|
+
Account: login logout whoami token config
|
|
521
|
+
|
|
522
|
+
`pm help <topic>` for verb-specific help with examples.
|
|
523
|
+
`pm --help` lists every command."""
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ── Suppression: PM_AGENT_DENIED_ACTIONS ─────────────────────────────
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _denied_actions_from_env() -> "frozenset[str]":
|
|
530
|
+
"""Parse ``PM_AGENT_DENIED_ACTIONS`` into a set of canonical_actions.
|
|
531
|
+
|
|
532
|
+
Empty / unset env var → empty set (no suppression). Whitespace
|
|
533
|
+
around commas tolerated. Unknown entries are kept as-is — the
|
|
534
|
+
canonical_action format is ``<noun>.<verb>``, but we don't validate
|
|
535
|
+
here; help suppression is best-effort and pm_tool.py is the
|
|
536
|
+
real enforcement point.
|
|
537
|
+
"""
|
|
538
|
+
raw = os.environ.get("PM_AGENT_DENIED_ACTIONS", "")
|
|
539
|
+
return frozenset({x.strip() for x in raw.split(",") if x.strip()})
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _canonical_action_for(group: str, verb: str) -> str:
|
|
543
|
+
"""Map (group, verb) to ``<noun>.<verb>`` for deny-list matching.
|
|
544
|
+
|
|
545
|
+
Mirrors prefect/queryme/agent/canonical_actions.py's static map for
|
|
546
|
+
the typed-group case. Block-plumbing keeps ``block.<verb>`` here
|
|
547
|
+
(without uid-based type discrimination) because the help page
|
|
548
|
+
describes the verb generically — runtime dispatch will resolve to
|
|
549
|
+
a typed action via the real resolver.
|
|
550
|
+
"""
|
|
551
|
+
noun = _GROUP_TO_NOUN.get(group, group)
|
|
552
|
+
return f"{noun}.{verb}"
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _filter_verbs(
|
|
556
|
+
group: str,
|
|
557
|
+
verbs: Dict[str, str],
|
|
558
|
+
denied: "frozenset[str]",
|
|
559
|
+
) -> Dict[str, str]:
|
|
560
|
+
if not denied:
|
|
561
|
+
return verbs
|
|
562
|
+
return {
|
|
563
|
+
verb: desc
|
|
564
|
+
for verb, desc in verbs.items()
|
|
565
|
+
if _canonical_action_for(group, verb) not in denied
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ── Page rendering ────────────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _render_group_page(group: str, denied: "frozenset[str]") -> str:
|
|
573
|
+
spec = _GROUP_PAGES[group]
|
|
574
|
+
visible_verbs = _filter_verbs(group, spec["verbs"], denied)
|
|
575
|
+
|
|
576
|
+
lines: List[str] = [f"pm {group} — {spec['summary']}", ""]
|
|
577
|
+
|
|
578
|
+
if not visible_verbs:
|
|
579
|
+
lines.append(
|
|
580
|
+
"No verbs available in the current agent mode. "
|
|
581
|
+
"Check PM_AGENT_DENIED_ACTIONS or use a wider mode.",
|
|
582
|
+
)
|
|
583
|
+
return "\n".join(lines)
|
|
584
|
+
|
|
585
|
+
lines.append("Verbs:")
|
|
586
|
+
width = max(len(v) for v in visible_verbs) + 2
|
|
587
|
+
for verb, desc in visible_verbs.items():
|
|
588
|
+
lines.append(f" {verb.ljust(width)}{desc}")
|
|
589
|
+
|
|
590
|
+
if "body_fields" in spec and spec["body_fields"]:
|
|
591
|
+
lines.append("")
|
|
592
|
+
lines.append("Body fields:")
|
|
593
|
+
lines.append(spec["body_fields"])
|
|
594
|
+
|
|
595
|
+
lines.append("")
|
|
596
|
+
lines.append("Example:")
|
|
597
|
+
lines.append(spec["example"])
|
|
598
|
+
|
|
599
|
+
return "\n".join(lines)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _known_topics() -> List[str]:
|
|
603
|
+
return sorted(set(_GROUP_PAGES) | set(_PLAIN_PAGES))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# ── Click command ────────────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@click.command(name="help")
|
|
610
|
+
@click.argument("topic", required=False)
|
|
611
|
+
@click.pass_context
|
|
612
|
+
def help_cmd(ctx: click.Context, topic: Optional[str]) -> None:
|
|
613
|
+
"""Show compact help for a group, with examples.
|
|
614
|
+
|
|
615
|
+
With no topic, prints the surface map; with a topic (e.g. pm help sql),
|
|
616
|
+
prints that group's page. This is the view the AI agent reads.
|
|
617
|
+
|
|
618
|
+
Suppresses verbs denied via PM_AGENT_DENIED_ACTIONS so agent-mode
|
|
619
|
+
help only surfaces invokable actions.
|
|
620
|
+
"""
|
|
621
|
+
denied = _denied_actions_from_env()
|
|
622
|
+
if topic is None:
|
|
623
|
+
click.echo(_TOP_LEVEL_PAGE)
|
|
624
|
+
return
|
|
625
|
+
if topic in _PLAIN_PAGES:
|
|
626
|
+
click.echo(_PLAIN_PAGES[topic])
|
|
627
|
+
return
|
|
628
|
+
if topic not in _GROUP_PAGES:
|
|
629
|
+
click.echo(
|
|
630
|
+
f"No help for {topic!r}. Known topics: {', '.join(_known_topics())}",
|
|
631
|
+
err=True,
|
|
632
|
+
)
|
|
633
|
+
ctx.exit(1)
|
|
634
|
+
click.echo(_render_group_page(topic, denied))
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# Exposed for tests + introspection.
|
|
638
|
+
__all__ = [
|
|
639
|
+
"help_cmd",
|
|
640
|
+
"_GROUP_PAGES",
|
|
641
|
+
"_GROUP_TO_NOUN",
|
|
642
|
+
"_TOP_LEVEL_PAGE",
|
|
643
|
+
]
|