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/db.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Database commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from pm._alias import AliasedGroup
|
|
9
|
+
from pm.client import PMClient
|
|
10
|
+
from pm.commands._helpers import json_option, limit_option, name_filter_option, select_rows
|
|
11
|
+
from pm.output import emit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="db", cls=AliasedGroup, aliases={"ls": "list"})
|
|
15
|
+
def db() -> None:
|
|
16
|
+
"""Databases, their schemas and tables.
|
|
17
|
+
|
|
18
|
+
DB_REF is the database uid or name.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@db.command(name="list")
|
|
23
|
+
@name_filter_option
|
|
24
|
+
@limit_option
|
|
25
|
+
@json_option
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def ls(ctx: click.Context, name_filter: Optional[str], limit: Optional[int]) -> None:
|
|
28
|
+
"""List databases configured in the current workspace."""
|
|
29
|
+
profile = ctx.obj["profile"]
|
|
30
|
+
json_output = ctx.obj["json"]
|
|
31
|
+
|
|
32
|
+
client = PMClient.from_profile(profile)
|
|
33
|
+
items = select_rows(
|
|
34
|
+
client.databases.list(), name=name_filter, fields=("database_name",), limit=limit
|
|
35
|
+
)
|
|
36
|
+
client.persist()
|
|
37
|
+
|
|
38
|
+
emit(
|
|
39
|
+
items,
|
|
40
|
+
json_output=json_output,
|
|
41
|
+
columns=[
|
|
42
|
+
("UID", "uid"),
|
|
43
|
+
("NAME", "database_name"),
|
|
44
|
+
("BACKEND", "backend"),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@db.command(name="schemas")
|
|
50
|
+
@click.argument("db_ref")
|
|
51
|
+
@click.option(
|
|
52
|
+
"--force",
|
|
53
|
+
is_flag=True,
|
|
54
|
+
help="Bypass the cached schema list and refresh from the source.",
|
|
55
|
+
)
|
|
56
|
+
@json_option
|
|
57
|
+
@click.pass_context
|
|
58
|
+
def schemas(ctx: click.Context, db_ref: str, force: bool) -> None:
|
|
59
|
+
"""List the schemas in a database."""
|
|
60
|
+
profile = ctx.obj["profile"]
|
|
61
|
+
json_output = ctx.obj["json"]
|
|
62
|
+
|
|
63
|
+
client = PMClient.from_profile(profile)
|
|
64
|
+
names = client.databases.schemas(db_ref, force=force)
|
|
65
|
+
client.persist()
|
|
66
|
+
|
|
67
|
+
if json_output:
|
|
68
|
+
emit(names, json_output=True)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
emit(
|
|
72
|
+
[{"name": s} for s in names],
|
|
73
|
+
json_output=False,
|
|
74
|
+
columns=[("SCHEMA", "name")],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@db.command(name="tables")
|
|
79
|
+
@click.argument("db_ref")
|
|
80
|
+
@click.argument("schema")
|
|
81
|
+
@json_option
|
|
82
|
+
@click.pass_context
|
|
83
|
+
def tables(ctx: click.Context, db_ref: str, schema: str) -> None:
|
|
84
|
+
"""List the tables and views in a schema."""
|
|
85
|
+
profile = ctx.obj["profile"]
|
|
86
|
+
json_output = ctx.obj["json"]
|
|
87
|
+
|
|
88
|
+
client = PMClient.from_profile(profile)
|
|
89
|
+
items = client.databases.tables(db_ref, schema)
|
|
90
|
+
client.persist()
|
|
91
|
+
|
|
92
|
+
emit(
|
|
93
|
+
items,
|
|
94
|
+
json_output=json_output,
|
|
95
|
+
columns=[
|
|
96
|
+
("NAME", "name"),
|
|
97
|
+
("TYPE", "type"),
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@db.command(name="describe")
|
|
103
|
+
@click.argument("db_ref")
|
|
104
|
+
@click.argument("schema")
|
|
105
|
+
@click.argument("table")
|
|
106
|
+
@json_option
|
|
107
|
+
@click.pass_context
|
|
108
|
+
def describe(ctx: click.Context, db_ref: str, schema: str, table: str) -> None:
|
|
109
|
+
"""Show a table's columns, keys and indexes."""
|
|
110
|
+
profile = ctx.obj["profile"]
|
|
111
|
+
json_output = ctx.obj["json"]
|
|
112
|
+
|
|
113
|
+
client = PMClient.from_profile(profile)
|
|
114
|
+
meta = client.databases.table_metadata(db_ref, table, schema)
|
|
115
|
+
client.persist()
|
|
116
|
+
|
|
117
|
+
if json_output:
|
|
118
|
+
emit(meta, json_output=True)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
click.echo(f"Table: {schema}.{table}")
|
|
122
|
+
if meta.get("comment"):
|
|
123
|
+
click.echo(f"Comment: {meta['comment']}")
|
|
124
|
+
|
|
125
|
+
cols = meta.get("columns") or []
|
|
126
|
+
if cols:
|
|
127
|
+
click.echo("")
|
|
128
|
+
emit(
|
|
129
|
+
cols,
|
|
130
|
+
json_output=False,
|
|
131
|
+
columns=[
|
|
132
|
+
("NAME", "name"),
|
|
133
|
+
("TYPE", "longType"),
|
|
134
|
+
("KEYS", lambda r: ",".join(
|
|
135
|
+
k.get("type", "") for k in (r.get("keys") or [])
|
|
136
|
+
)),
|
|
137
|
+
("COMMENT", lambda r: r.get("comment") or ""),
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
fks = meta.get("foreignKeys") or []
|
|
142
|
+
if fks:
|
|
143
|
+
click.echo("")
|
|
144
|
+
click.echo("Foreign keys:")
|
|
145
|
+
for fk in fks:
|
|
146
|
+
src = ", ".join(fk.get("column_names") or [])
|
|
147
|
+
ref_table = fk.get("referred_table") or "?"
|
|
148
|
+
ref_cols = ", ".join(fk.get("referred_columns") or [])
|
|
149
|
+
ref_schema = fk.get("referred_schema") or ""
|
|
150
|
+
ref_full = f"{ref_schema}.{ref_table}" if ref_schema else ref_table
|
|
151
|
+
click.echo(f" ({src}) -> {ref_full}({ref_cols})")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@db.command(name="select-star")
|
|
155
|
+
@click.argument("db_ref")
|
|
156
|
+
@click.argument("schema")
|
|
157
|
+
@click.argument("table")
|
|
158
|
+
@click.option(
|
|
159
|
+
"--limit",
|
|
160
|
+
type=int,
|
|
161
|
+
default=100,
|
|
162
|
+
show_default=True,
|
|
163
|
+
help="Row limit written into the generated SQL (max 10000).",
|
|
164
|
+
)
|
|
165
|
+
@json_option
|
|
166
|
+
@click.pass_context
|
|
167
|
+
def select_star(
|
|
168
|
+
ctx: click.Context, db_ref: str, schema: str, table: str, limit: int
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Print a SELECT * statement for a table (not run)."""
|
|
171
|
+
profile = ctx.obj["profile"]
|
|
172
|
+
json_output = ctx.obj["json"]
|
|
173
|
+
|
|
174
|
+
client = PMClient.from_profile(profile)
|
|
175
|
+
sql = client.databases.select_star_sql(db_ref, table, schema, limit=limit)
|
|
176
|
+
client.persist()
|
|
177
|
+
|
|
178
|
+
if json_output:
|
|
179
|
+
emit({"sql": sql}, json_output=True)
|
|
180
|
+
return
|
|
181
|
+
click.echo(sql)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@db.command(name="exists")
|
|
185
|
+
@click.argument("db_ref")
|
|
186
|
+
@click.argument("schema")
|
|
187
|
+
@click.argument("table")
|
|
188
|
+
@json_option
|
|
189
|
+
@click.pass_context
|
|
190
|
+
def exists(ctx: click.Context, db_ref: str, schema: str, table: str) -> None:
|
|
191
|
+
"""Check whether a table or view exists.
|
|
192
|
+
|
|
193
|
+
Exits 0 if it exists and 1 if not, so it works in shell conditions:
|
|
194
|
+
if pm database exists ...; then ...; fi. With --json the exit code is
|
|
195
|
+
always 0 and the "exists" field carries the answer.
|
|
196
|
+
"""
|
|
197
|
+
profile = ctx.obj["profile"]
|
|
198
|
+
json_output = ctx.obj["json"]
|
|
199
|
+
|
|
200
|
+
client = PMClient.from_profile(profile)
|
|
201
|
+
info = client.databases.table_exists(db_ref, table, schema)
|
|
202
|
+
client.persist()
|
|
203
|
+
|
|
204
|
+
if json_output:
|
|
205
|
+
emit(info, json_output=True)
|
|
206
|
+
# In --json mode (agent / programmatic), exit cleanly — the JSON
|
|
207
|
+
# carries the boolean. Exit-code signalling is reserved for shell
|
|
208
|
+
# use of the human-readable mode below.
|
|
209
|
+
return
|
|
210
|
+
kind = info.get("type") or "-"
|
|
211
|
+
resolved = info.get("resolved_name") or "-"
|
|
212
|
+
click.echo(f"exists={info.get('exists')} type={kind} resolved={resolved}")
|
|
213
|
+
if not info.get("exists"):
|
|
214
|
+
ctx.exit(1)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@db.command(name="functions")
|
|
218
|
+
@click.argument("db_ref")
|
|
219
|
+
@click.option(
|
|
220
|
+
"--filter",
|
|
221
|
+
"filter_",
|
|
222
|
+
default="",
|
|
223
|
+
help="Keep only names containing this text (case-insensitive).",
|
|
224
|
+
)
|
|
225
|
+
@click.option(
|
|
226
|
+
"--limit",
|
|
227
|
+
type=int,
|
|
228
|
+
default=100,
|
|
229
|
+
show_default=True,
|
|
230
|
+
help="Maximum number of names to return (max 500).",
|
|
231
|
+
)
|
|
232
|
+
@json_option
|
|
233
|
+
@click.pass_context
|
|
234
|
+
def functions(ctx: click.Context, db_ref: str, filter_: str, limit: int) -> None:
|
|
235
|
+
"""List the SQL functions the database supports."""
|
|
236
|
+
profile = ctx.obj["profile"]
|
|
237
|
+
json_output = ctx.obj["json"]
|
|
238
|
+
|
|
239
|
+
client = PMClient.from_profile(profile)
|
|
240
|
+
result = client.databases.function_names(db_ref, filter=filter_, limit=limit)
|
|
241
|
+
client.persist()
|
|
242
|
+
|
|
243
|
+
if json_output:
|
|
244
|
+
emit(result, json_output=True)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
note = result.get("note")
|
|
248
|
+
if note:
|
|
249
|
+
click.echo(f"Note: {note}")
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
for name in result.get("functions") or []:
|
|
253
|
+
click.echo(name)
|
|
254
|
+
total = result.get("total", 0)
|
|
255
|
+
count = result.get("count", 0)
|
|
256
|
+
if total and count < total:
|
|
257
|
+
click.echo(f"-- truncated: {count} of {total} (pass --limit to widen).")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@db.command(name="search")
|
|
261
|
+
@click.argument("db_ref")
|
|
262
|
+
@click.argument("pattern")
|
|
263
|
+
@click.option(
|
|
264
|
+
"--schema",
|
|
265
|
+
default="",
|
|
266
|
+
help="Search one schema only (default: every non-system schema).",
|
|
267
|
+
)
|
|
268
|
+
@json_option
|
|
269
|
+
@click.pass_context
|
|
270
|
+
def search(ctx: click.Context, db_ref: str, pattern: str, schema: str) -> None:
|
|
271
|
+
"""Find tables and views whose name contains PATTERN."""
|
|
272
|
+
profile = ctx.obj["profile"]
|
|
273
|
+
json_output = ctx.obj["json"]
|
|
274
|
+
|
|
275
|
+
client = PMClient.from_profile(profile)
|
|
276
|
+
result = client.databases.search_tables(db_ref, pattern, schema=schema)
|
|
277
|
+
client.persist()
|
|
278
|
+
|
|
279
|
+
if json_output:
|
|
280
|
+
emit(result, json_output=True)
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
emit(
|
|
284
|
+
result.get("results") or [],
|
|
285
|
+
json_output=False,
|
|
286
|
+
columns=[
|
|
287
|
+
("SCHEMA", "schema"),
|
|
288
|
+
("NAME", "name"),
|
|
289
|
+
("TYPE", "type"),
|
|
290
|
+
],
|
|
291
|
+
)
|
pm/commands/email.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""`pm email` — create + send email blocks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pm.commands._block_factory import make_block_group
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
email = make_block_group(
|
|
8
|
+
"email",
|
|
9
|
+
"email",
|
|
10
|
+
help_text="Email blocks.\n\nBlock type: email.",
|
|
11
|
+
crud_verbs=("create", "list", "get"),
|
|
12
|
+
execute_verb="send",
|
|
13
|
+
)
|
pm/commands/explore.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""`pm explore` — create explore (saved-view) blocks.
|
|
2
|
+
|
|
3
|
+
No execute verb today — explore blocks are saved-view definitions that
|
|
4
|
+
the UI renders; there's no server-side "run" path that produces a
|
|
5
|
+
deliverable artifact. ``list`` and ``get`` are exposed for parity with
|
|
6
|
+
the other typed-block groups so an agent that guesses ``pm explore
|
|
7
|
+
list`` lands on a working command instead of an error envelope (prod
|
|
8
|
+
session Gn6eNEko2y, 2026-06-09).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pm.commands._block_factory import make_block_group
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
explore = make_block_group(
|
|
16
|
+
"explore",
|
|
17
|
+
"explore",
|
|
18
|
+
help_text="Explore blocks (saved data views).\n\nBlock type: explore.",
|
|
19
|
+
crud_verbs=("create", "list", "get", "update"),
|
|
20
|
+
)
|
pm/commands/favorites.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Favorite commands (ls/create/delete only — backend supports more but FE doesn't)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from pm._alias import AliasedGroup
|
|
9
|
+
from pm.client import PMClient
|
|
10
|
+
from pm.commands._helpers import (
|
|
11
|
+
confirm_delete,
|
|
12
|
+
json_option,
|
|
13
|
+
merge_fields,
|
|
14
|
+
parse_data,
|
|
15
|
+
report_deleted,
|
|
16
|
+
yes_option,
|
|
17
|
+
limit_option,
|
|
18
|
+
select_rows,
|
|
19
|
+
)
|
|
20
|
+
from pm.output import emit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(name="favorites", cls=AliasedGroup, aliases={"ls": "list"})
|
|
24
|
+
def favorites() -> None:
|
|
25
|
+
"""Your favorite reports and other objects."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@favorites.command(name="list")
|
|
29
|
+
@limit_option
|
|
30
|
+
@json_option
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def ls(ctx: click.Context, limit: Optional[int]) -> None:
|
|
33
|
+
"""List your favorites."""
|
|
34
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
35
|
+
items = select_rows(client.favorites.list(), limit=limit)
|
|
36
|
+
client.persist()
|
|
37
|
+
emit(
|
|
38
|
+
items,
|
|
39
|
+
json_output=ctx.obj["json"],
|
|
40
|
+
columns=[
|
|
41
|
+
("UID", "uid"),
|
|
42
|
+
("CLASS", "class_name"),
|
|
43
|
+
("OBJ_ID", "obj_id"),
|
|
44
|
+
],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@favorites.command(name="create")
|
|
49
|
+
@click.option("--class-name", required=True,
|
|
50
|
+
help="Kind of object to favorite, e.g. Report.")
|
|
51
|
+
@click.option("--obj-id", type=int, required=True,
|
|
52
|
+
help="Numeric id of the object (from pm report get --json).")
|
|
53
|
+
@click.option("--data", default=None, help="Extra fields as a JSON object.")
|
|
54
|
+
@json_option
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def create(ctx: click.Context, class_name: str, obj_id: int, data: Optional[str]) -> None:
|
|
57
|
+
"""Add an object to your favorites."""
|
|
58
|
+
payload = merge_fields(parse_data(data), {"class_name": class_name, "obj_id": obj_id})
|
|
59
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
60
|
+
fav = client.favorites.create(payload)
|
|
61
|
+
client.persist()
|
|
62
|
+
emit(fav, json_output=ctx.obj["json"])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@favorites.command(name="delete")
|
|
66
|
+
@click.argument("uid")
|
|
67
|
+
@yes_option
|
|
68
|
+
@json_option
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def delete(ctx: click.Context, uid: str, yes: bool) -> None:
|
|
71
|
+
"""Remove a favorite."""
|
|
72
|
+
if not yes:
|
|
73
|
+
confirm_delete("favorite", uid)
|
|
74
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
75
|
+
client.favorites.delete(uid)
|
|
76
|
+
client.persist()
|
|
77
|
+
report_deleted(ctx, "favorite", uid)
|
pm/commands/folders.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Folder commands (full CRUD)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from pm._alias import AliasedGroup
|
|
9
|
+
from pm.client import PMClient
|
|
10
|
+
from pm.commands._helpers import (
|
|
11
|
+
confirm_delete,
|
|
12
|
+
json_option,
|
|
13
|
+
merge_fields,
|
|
14
|
+
parse_data,
|
|
15
|
+
peek_name,
|
|
16
|
+
report_deleted,
|
|
17
|
+
yes_option,
|
|
18
|
+
limit_option,
|
|
19
|
+
name_filter_option,
|
|
20
|
+
select_rows,
|
|
21
|
+
)
|
|
22
|
+
from pm.output import emit
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group(name="folders", cls=AliasedGroup, aliases={"ls": "list"})
|
|
26
|
+
def folders() -> None:
|
|
27
|
+
"""Folders that organize reports."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@folders.command(name="list")
|
|
31
|
+
@name_filter_option
|
|
32
|
+
@limit_option
|
|
33
|
+
@json_option
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def ls(ctx: click.Context, name_filter: Optional[str], limit: Optional[int]) -> None:
|
|
36
|
+
"""List folders in the current workspace."""
|
|
37
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
38
|
+
items = select_rows(client.folders.list(), name=name_filter, limit=limit)
|
|
39
|
+
client.persist()
|
|
40
|
+
emit(
|
|
41
|
+
items,
|
|
42
|
+
json_output=ctx.obj["json"],
|
|
43
|
+
columns=[("UID", "uid"), ("NAME", "name"), ("PARENT_ID", "parent_id"), ("ID", "id")],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@folders.command(name="get")
|
|
48
|
+
@click.argument("uid")
|
|
49
|
+
@json_option
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def get(ctx: click.Context, uid: str) -> None:
|
|
52
|
+
"""Show one folder."""
|
|
53
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
54
|
+
folder = client.folders.get(uid)
|
|
55
|
+
client.persist()
|
|
56
|
+
emit(folder, json_output=ctx.obj["json"])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@folders.command(name="create")
|
|
60
|
+
@click.option("--name", required=True, help="Folder name.")
|
|
61
|
+
@click.option("--parent-id", type=int, default=None,
|
|
62
|
+
help="Numeric id of the parent folder (from pm folder list). Default: top level.")
|
|
63
|
+
@click.option("--data", default=None, help="Extra fields as a JSON object.")
|
|
64
|
+
@json_option
|
|
65
|
+
@click.pass_context
|
|
66
|
+
def create(ctx: click.Context, name: str, parent_id: Optional[int], data: Optional[str]) -> None:
|
|
67
|
+
"""Create a folder."""
|
|
68
|
+
payload = merge_fields(parse_data(data), {"name": name, "parent_id": parent_id})
|
|
69
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
70
|
+
folder = client.folders.create(payload)
|
|
71
|
+
client.persist()
|
|
72
|
+
emit(folder, json_output=ctx.obj["json"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@folders.command(name="update")
|
|
76
|
+
@click.argument("uid")
|
|
77
|
+
@click.option("--name", default=None, help="Rename the folder.")
|
|
78
|
+
@click.option("--parent-id", type=int, default=None,
|
|
79
|
+
help="Move under this parent folder (numeric id from pm folder list).")
|
|
80
|
+
@click.option("--data", default=None, help="Extra fields as a JSON object.")
|
|
81
|
+
@json_option
|
|
82
|
+
@click.pass_context
|
|
83
|
+
def update(
|
|
84
|
+
ctx: click.Context,
|
|
85
|
+
uid: str,
|
|
86
|
+
name: Optional[str],
|
|
87
|
+
parent_id: Optional[int],
|
|
88
|
+
data: Optional[str],
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Update a folder."""
|
|
91
|
+
payload = merge_fields(parse_data(data), {"name": name, "parent_id": parent_id})
|
|
92
|
+
if not payload:
|
|
93
|
+
raise click.UsageError("Nothing to update; pass --name, --parent-id, or --data.")
|
|
94
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
95
|
+
folder = client.folders.update(uid, payload)
|
|
96
|
+
client.persist()
|
|
97
|
+
emit(folder, json_output=ctx.obj["json"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@folders.command(name="delete")
|
|
101
|
+
@click.argument("uid")
|
|
102
|
+
@yes_option
|
|
103
|
+
@json_option
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def delete(ctx: click.Context, uid: str, yes: bool) -> None:
|
|
106
|
+
"""Delete a folder."""
|
|
107
|
+
if not yes:
|
|
108
|
+
name = peek_name(lambda: PMClient.from_profile(ctx.obj["profile"]).folders.get(uid), "name")
|
|
109
|
+
confirm_delete("folder", uid, name)
|
|
110
|
+
client = PMClient.from_profile(ctx.obj["profile"])
|
|
111
|
+
client.folders.delete(uid)
|
|
112
|
+
client.persist()
|
|
113
|
+
report_deleted(ctx, "folder", uid)
|