pipefy-cli 0.3.0a1__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.
- pipefy_cli/__init__.py +7 -0
- pipefy_cli/_docs.py +3 -0
- pipefy_cli/auth.py +225 -0
- pipefy_cli/commands/__init__.py +5 -0
- pipefy_cli/commands/_common.py +359 -0
- pipefy_cli/commands/agent.py +358 -0
- pipefy_cli/commands/ai_automation.py +318 -0
- pipefy_cli/commands/attachment.py +122 -0
- pipefy_cli/commands/audit.py +63 -0
- pipefy_cli/commands/auth.py +519 -0
- pipefy_cli/commands/automation.py +529 -0
- pipefy_cli/commands/card.py +439 -0
- pipefy_cli/commands/email.py +169 -0
- pipefy_cli/commands/export.py +61 -0
- pipefy_cli/commands/field.py +113 -0
- pipefy_cli/commands/field_condition.py +201 -0
- pipefy_cli/commands/graphql.py +110 -0
- pipefy_cli/commands/introspect.py +110 -0
- pipefy_cli/commands/label.py +109 -0
- pipefy_cli/commands/member.py +126 -0
- pipefy_cli/commands/org.py +60 -0
- pipefy_cli/commands/phase.py +264 -0
- pipefy_cli/commands/pipe.py +167 -0
- pipefy_cli/commands/portal.py +871 -0
- pipefy_cli/commands/record.py +219 -0
- pipefy_cli/commands/relation.py +173 -0
- pipefy_cli/commands/report_org.py +241 -0
- pipefy_cli/commands/report_pipe.py +290 -0
- pipefy_cli/commands/table.py +151 -0
- pipefy_cli/commands/usage.py +100 -0
- pipefy_cli/commands/webhook.py +141 -0
- pipefy_cli/main.py +132 -0
- pipefy_cli/output/__init__.py +8 -0
- pipefy_cli/output/json_renderer.py +24 -0
- pipefy_cli/output/rich_renderer.py +81 -0
- pipefy_cli/settings.py +66 -0
- pipefy_cli-0.3.0a1.dist-info/METADATA +104 -0
- pipefy_cli-0.3.0a1.dist-info/RECORD +40 -0
- pipefy_cli-0.3.0a1.dist-info/WHEEL +4 -0
- pipefy_cli-0.3.0a1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Pipe-level reports (CRUD, columns, export)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import PipefyClient
|
|
9
|
+
|
|
10
|
+
from pipefy_cli.commands._common import (
|
|
11
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
12
|
+
confirm_destructive,
|
|
13
|
+
parse_json_object,
|
|
14
|
+
parse_json_value,
|
|
15
|
+
prepare_report_cards_filter_cli,
|
|
16
|
+
resource_id_argument,
|
|
17
|
+
run_cli_command,
|
|
18
|
+
run_pipefy_client_coroutine,
|
|
19
|
+
write_export_csv_to_stdout,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
report_pipe_app = typer.Typer(help="Pipe reports.", no_args_is_help=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_order(raw: str | None) -> dict[str, Any] | None:
|
|
26
|
+
if raw is None or not raw.strip():
|
|
27
|
+
return None
|
|
28
|
+
parsed = parse_json_value(raw, "--order")
|
|
29
|
+
if not isinstance(parsed, dict):
|
|
30
|
+
raise typer.BadParameter("--order must be a JSON object")
|
|
31
|
+
return parsed
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@report_pipe_app.command("list")
|
|
35
|
+
def report_pipe_list(
|
|
36
|
+
ctx: typer.Context,
|
|
37
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe UUID."),
|
|
38
|
+
first: int = typer.Option(30, "--first", help="Page size."),
|
|
39
|
+
after: str | None = typer.Option(None, "--after", help="Pagination cursor."),
|
|
40
|
+
search: str | None = typer.Option(None, "--search"),
|
|
41
|
+
report_id: str | None = typer.Option(None, "--report-id"),
|
|
42
|
+
order: str | None = typer.Option(None, "--order", help="JSON sort object."),
|
|
43
|
+
json_out: bool = typer.Option(
|
|
44
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
45
|
+
),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""List pipe reports (``get_pipe_reports``)."""
|
|
48
|
+
order_obj = _parse_order(order)
|
|
49
|
+
|
|
50
|
+
async def factory(client: PipefyClient):
|
|
51
|
+
return await client.get_pipe_reports(
|
|
52
|
+
pipe,
|
|
53
|
+
first=first,
|
|
54
|
+
after=after,
|
|
55
|
+
search=search,
|
|
56
|
+
report_id=report_id,
|
|
57
|
+
order=order_obj,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
run_cli_command(ctx, json_out, factory)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@report_pipe_app.command("get")
|
|
64
|
+
def report_pipe_get(
|
|
65
|
+
ctx: typer.Context,
|
|
66
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe UUID."),
|
|
67
|
+
report_id: str = typer.Option(..., "--report-id", help="Pipe report id."),
|
|
68
|
+
json_out: bool = typer.Option(
|
|
69
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
70
|
+
),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Fetch one pipe report (``get_pipe_report``)."""
|
|
73
|
+
|
|
74
|
+
async def factory(client: PipefyClient):
|
|
75
|
+
raw = await client.get_pipe_reports(pipe, first=1, report_id=report_id)
|
|
76
|
+
edges = (raw.get("pipeReports") or {}).get("edges") or []
|
|
77
|
+
node = edges[0].get("node") if edges and isinstance(edges[0], dict) else None
|
|
78
|
+
if not node:
|
|
79
|
+
return {
|
|
80
|
+
"success": False,
|
|
81
|
+
"message": f"Pipe report not found (id={report_id}).",
|
|
82
|
+
}
|
|
83
|
+
return {"success": True, "pipeReport": node}
|
|
84
|
+
|
|
85
|
+
run_cli_command(ctx, json_out, factory)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@report_pipe_app.command("columns")
|
|
89
|
+
def report_pipe_columns(
|
|
90
|
+
ctx: typer.Context,
|
|
91
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe UUID."),
|
|
92
|
+
json_out: bool = typer.Option(
|
|
93
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
94
|
+
),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""List report column definitions (``get_pipe_report_columns``)."""
|
|
97
|
+
|
|
98
|
+
async def factory(client: PipefyClient):
|
|
99
|
+
return await client.get_pipe_report_columns(pipe)
|
|
100
|
+
|
|
101
|
+
run_cli_command(ctx, json_out, factory)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@report_pipe_app.command("filterable-fields")
|
|
105
|
+
def report_pipe_filterable_fields(
|
|
106
|
+
ctx: typer.Context,
|
|
107
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe UUID."),
|
|
108
|
+
json_out: bool = typer.Option(
|
|
109
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
110
|
+
),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""List filterable fields for pipe reports (``get_pipe_report_filterable_fields``)."""
|
|
113
|
+
|
|
114
|
+
async def factory(client: PipefyClient):
|
|
115
|
+
return await client.get_pipe_report_filterable_fields(pipe)
|
|
116
|
+
|
|
117
|
+
run_cli_command(ctx, json_out, factory)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@report_pipe_app.command("create")
|
|
121
|
+
def report_pipe_create(
|
|
122
|
+
ctx: typer.Context,
|
|
123
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
124
|
+
name: str = typer.Option(..., "--name", "-n"),
|
|
125
|
+
fields: str | None = typer.Option(
|
|
126
|
+
None, "--fields", help="Optional JSON array of column internal names."
|
|
127
|
+
),
|
|
128
|
+
filter_json: str | None = typer.Option(
|
|
129
|
+
None, "--filter", help="Optional JSON filter object."
|
|
130
|
+
),
|
|
131
|
+
formulas: str | None = typer.Option(
|
|
132
|
+
None, "--formulas", help="Optional JSON array of arrays."
|
|
133
|
+
),
|
|
134
|
+
json_out: bool = typer.Option(
|
|
135
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
136
|
+
),
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Create a pipe report (``create_pipe_report``)."""
|
|
139
|
+
fields_list = parse_json_value(fields, "--fields") if fields else None
|
|
140
|
+
if fields_list is not None and not isinstance(fields_list, list):
|
|
141
|
+
raise typer.BadParameter("--fields must be a JSON array")
|
|
142
|
+
filt = prepare_report_cards_filter_cli(parse_json_object(filter_json, "--filter"))
|
|
143
|
+
formulas_val = parse_json_value(formulas, "--formulas") if formulas else None
|
|
144
|
+
if formulas_val is not None and not isinstance(formulas_val, list):
|
|
145
|
+
raise typer.BadParameter("--formulas must be a JSON array")
|
|
146
|
+
|
|
147
|
+
async def factory(client: PipefyClient):
|
|
148
|
+
return await client.create_pipe_report(
|
|
149
|
+
pipe,
|
|
150
|
+
name.strip(),
|
|
151
|
+
fields=fields_list,
|
|
152
|
+
filter=filt,
|
|
153
|
+
formulas=formulas_val,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
run_cli_command(ctx, json_out, factory)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@report_pipe_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
160
|
+
def report_pipe_update(
|
|
161
|
+
ctx: typer.Context,
|
|
162
|
+
report_id: str = resource_id_argument(help="Pipe report id."),
|
|
163
|
+
name: str | None = typer.Option(None, "--name", "-n"),
|
|
164
|
+
color: str | None = typer.Option(None, "--color"),
|
|
165
|
+
fields: str | None = typer.Option(None, "--fields"),
|
|
166
|
+
filter_json: str | None = typer.Option(None, "--filter"),
|
|
167
|
+
formulas: str | None = typer.Option(None, "--formulas"),
|
|
168
|
+
featured_field: str | None = typer.Option(None, "--featured-field"),
|
|
169
|
+
json_out: bool = typer.Option(
|
|
170
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
171
|
+
),
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Update a pipe report (``update_pipe_report``)."""
|
|
174
|
+
fields_list = parse_json_value(fields, "--fields") if fields else None
|
|
175
|
+
if fields_list is not None and not isinstance(fields_list, list):
|
|
176
|
+
raise typer.BadParameter("--fields must be a JSON array")
|
|
177
|
+
filt = prepare_report_cards_filter_cli(parse_json_object(filter_json, "--filter"))
|
|
178
|
+
formulas_val = parse_json_value(formulas, "--formulas") if formulas else None
|
|
179
|
+
if formulas_val is not None and not isinstance(formulas_val, list):
|
|
180
|
+
raise typer.BadParameter("--formulas must be a JSON array")
|
|
181
|
+
|
|
182
|
+
async def factory(client: PipefyClient):
|
|
183
|
+
return await client.update_pipe_report(
|
|
184
|
+
report_id,
|
|
185
|
+
name=name.strip() if name else None,
|
|
186
|
+
color=color,
|
|
187
|
+
fields=fields_list,
|
|
188
|
+
filter=filt,
|
|
189
|
+
formulas=formulas_val,
|
|
190
|
+
featured_field=featured_field,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
run_cli_command(ctx, json_out, factory)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@report_pipe_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
197
|
+
def report_pipe_delete(
|
|
198
|
+
ctx: typer.Context,
|
|
199
|
+
report_id: str = resource_id_argument(help="Pipe report id."),
|
|
200
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
201
|
+
json_out: bool = typer.Option(
|
|
202
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
203
|
+
),
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Delete a pipe report (``delete_pipe_report``)."""
|
|
206
|
+
confirm_destructive(yes=yes, description=f"pipe report {report_id}", verb="delete")
|
|
207
|
+
|
|
208
|
+
async def factory(client: PipefyClient):
|
|
209
|
+
return await client.delete_pipe_report(report_id)
|
|
210
|
+
|
|
211
|
+
run_cli_command(ctx, json_out, factory)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@report_pipe_app.command("export")
|
|
215
|
+
def report_pipe_export(
|
|
216
|
+
ctx: typer.Context,
|
|
217
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
218
|
+
report_id: str = typer.Option(..., "--report-id", help="Pipe report id."),
|
|
219
|
+
format_: str = typer.Option(
|
|
220
|
+
"json",
|
|
221
|
+
"--format",
|
|
222
|
+
help="json: print mutation + poll metadata; csv: wait for fileURL and stream bytes.",
|
|
223
|
+
),
|
|
224
|
+
sort_by: str | None = typer.Option(
|
|
225
|
+
None, "--sort-by", help="JSON ReportSortDirectionInput."
|
|
226
|
+
),
|
|
227
|
+
filter_json: str | None = typer.Option(None, "--filter"),
|
|
228
|
+
columns: str | None = typer.Option(
|
|
229
|
+
None, "--columns", help="JSON array of column ids."
|
|
230
|
+
),
|
|
231
|
+
poll_timeout: float = typer.Option(
|
|
232
|
+
90.0,
|
|
233
|
+
"--poll-timeout",
|
|
234
|
+
help="Seconds to poll export status before failing (CSV only).",
|
|
235
|
+
),
|
|
236
|
+
json_out: bool = typer.Option(
|
|
237
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
238
|
+
),
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Start a pipe report export (``export_pipe_report``); csv waits and streams the file."""
|
|
241
|
+
fmt = format_.strip().lower()
|
|
242
|
+
if fmt not in ("json", "csv"):
|
|
243
|
+
raise typer.BadParameter("--format must be json or csv")
|
|
244
|
+
if fmt == "csv" and json_out:
|
|
245
|
+
raise typer.BadParameter(
|
|
246
|
+
"--json is mutually exclusive with --format csv (CSV is written as raw bytes to stdout)."
|
|
247
|
+
)
|
|
248
|
+
sort_obj = parse_json_object(sort_by, "--sort-by")
|
|
249
|
+
filt = prepare_report_cards_filter_cli(parse_json_object(filter_json, "--filter"))
|
|
250
|
+
cols = parse_json_value(columns, "--columns") if columns else None
|
|
251
|
+
if cols is not None and not isinstance(cols, list):
|
|
252
|
+
raise typer.BadParameter("--columns must be a JSON array")
|
|
253
|
+
|
|
254
|
+
if fmt == "json":
|
|
255
|
+
|
|
256
|
+
async def factory(client: PipefyClient):
|
|
257
|
+
return await client.export_pipe_report(
|
|
258
|
+
pipe,
|
|
259
|
+
report_id,
|
|
260
|
+
sort_by=sort_obj,
|
|
261
|
+
filter=filt,
|
|
262
|
+
columns=cols,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
run_cli_command(ctx, json_out, factory)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
async def csv_factory(client: PipefyClient) -> None:
|
|
269
|
+
start = await client.export_pipe_report(
|
|
270
|
+
pipe,
|
|
271
|
+
report_id,
|
|
272
|
+
sort_by=sort_obj,
|
|
273
|
+
filter=filt,
|
|
274
|
+
columns=cols,
|
|
275
|
+
)
|
|
276
|
+
exp = (start.get("exportPipeReport") or {}).get("pipeReportExport") or {}
|
|
277
|
+
export_id = exp.get("id")
|
|
278
|
+
if not export_id:
|
|
279
|
+
typer.echo(
|
|
280
|
+
"Could not read export id from exportPipeReport response.", err=True
|
|
281
|
+
)
|
|
282
|
+
raise typer.Exit(1)
|
|
283
|
+
await write_export_csv_to_stdout(
|
|
284
|
+
export_id=str(export_id),
|
|
285
|
+
poll_fetch=client.get_pipe_report_export,
|
|
286
|
+
unwrap_status=lambda raw: (raw or {}).get("pipeReportExport") or {},
|
|
287
|
+
poll_timeout_seconds=poll_timeout,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
run_pipefy_client_coroutine(ctx, csv_factory, value_error_exit_code=1)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Database table subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import PipefyClient
|
|
9
|
+
|
|
10
|
+
from pipefy_cli.commands._common import (
|
|
11
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
12
|
+
confirm_destructive,
|
|
13
|
+
parse_json_object,
|
|
14
|
+
resource_id_argument,
|
|
15
|
+
run_cli_command,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
table_app = typer.Typer(help="Database table operations.", no_args_is_help=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@table_app.command("list")
|
|
22
|
+
def table_list(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
ids: str | None = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--ids",
|
|
27
|
+
help="Comma-separated table ids; when set, loads those tables via get_tables.",
|
|
28
|
+
),
|
|
29
|
+
name: str | None = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--name",
|
|
32
|
+
"-n",
|
|
33
|
+
help="Optional table name filter (ignored when --ids is set).",
|
|
34
|
+
),
|
|
35
|
+
first: int = typer.Option(
|
|
36
|
+
100,
|
|
37
|
+
"--first",
|
|
38
|
+
min=1,
|
|
39
|
+
max=500,
|
|
40
|
+
help="Max tables per organization when searching (1-500, default 100).",
|
|
41
|
+
),
|
|
42
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Search tables across organizations, or fetch specific tables by id."""
|
|
45
|
+
|
|
46
|
+
id_list: list[str] | None = None
|
|
47
|
+
if ids is not None and ids.strip():
|
|
48
|
+
id_list = [p.strip() for p in ids.split(",") if p.strip()]
|
|
49
|
+
if not id_list:
|
|
50
|
+
raise typer.BadParameter("--ids must list at least one table id.")
|
|
51
|
+
|
|
52
|
+
async def factory(client: PipefyClient):
|
|
53
|
+
if id_list is not None:
|
|
54
|
+
return await client.get_tables(id_list)
|
|
55
|
+
return await client.search_tables(name, first=first)
|
|
56
|
+
|
|
57
|
+
run_cli_command(ctx, json_out, factory)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@table_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
61
|
+
def table_get(
|
|
62
|
+
ctx: typer.Context,
|
|
63
|
+
table_id: str = resource_id_argument(help="Table id."),
|
|
64
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Load one table by id."""
|
|
67
|
+
|
|
68
|
+
async def factory(client: PipefyClient):
|
|
69
|
+
return await client.get_table(table_id)
|
|
70
|
+
|
|
71
|
+
run_cli_command(ctx, json_out, factory)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@table_app.command("create")
|
|
75
|
+
def table_create(
|
|
76
|
+
ctx: typer.Context,
|
|
77
|
+
name: str = typer.Argument(..., help="Table display name."),
|
|
78
|
+
org_id: str = typer.Option(
|
|
79
|
+
...,
|
|
80
|
+
"--org",
|
|
81
|
+
help="Organization id that will own the table.",
|
|
82
|
+
),
|
|
83
|
+
extra_json: str | None = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"--extra",
|
|
86
|
+
help="JSON object of extra CreateTableInput fields.",
|
|
87
|
+
),
|
|
88
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Create a database table."""
|
|
91
|
+
|
|
92
|
+
stripped = name.strip()
|
|
93
|
+
if not stripped:
|
|
94
|
+
typer.echo("Table name must be non-empty.", err=True)
|
|
95
|
+
raise typer.Exit(2)
|
|
96
|
+
extra = parse_json_object(extra_json, "--extra") or {}
|
|
97
|
+
|
|
98
|
+
async def factory(client: PipefyClient):
|
|
99
|
+
return await client.create_table(stripped, org_id, **extra)
|
|
100
|
+
|
|
101
|
+
run_cli_command(ctx, json_out, factory)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@table_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
105
|
+
def table_update(
|
|
106
|
+
ctx: typer.Context,
|
|
107
|
+
table_id: str = resource_id_argument(help="Table id."),
|
|
108
|
+
name: str | None = typer.Option(None, "--name"),
|
|
109
|
+
description: str | None = typer.Option(None, "--description", "-d"),
|
|
110
|
+
extra_json: str | None = typer.Option(
|
|
111
|
+
None,
|
|
112
|
+
"--extra",
|
|
113
|
+
help="JSON object merged into UpdateTableInput.",
|
|
114
|
+
),
|
|
115
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Update table attributes."""
|
|
118
|
+
|
|
119
|
+
extra = parse_json_object(extra_json, "--extra") or {}
|
|
120
|
+
attrs: dict[str, Any] = {}
|
|
121
|
+
if name is not None:
|
|
122
|
+
attrs["name"] = name
|
|
123
|
+
if description is not None:
|
|
124
|
+
attrs["description"] = description
|
|
125
|
+
attrs.update(extra)
|
|
126
|
+
if not attrs:
|
|
127
|
+
raise typer.BadParameter(
|
|
128
|
+
"Provide at least one of: --name, --description, --extra (non-empty)."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def factory(client: PipefyClient):
|
|
132
|
+
return await client.update_table(table_id, **attrs)
|
|
133
|
+
|
|
134
|
+
run_cli_command(ctx, json_out, factory)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@table_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
138
|
+
def table_delete(
|
|
139
|
+
ctx: typer.Context,
|
|
140
|
+
table_id: str = resource_id_argument(help="Table id."),
|
|
141
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
142
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Delete a database table permanently."""
|
|
145
|
+
|
|
146
|
+
confirm_destructive(yes=yes, description=f"table {table_id}")
|
|
147
|
+
|
|
148
|
+
async def factory(client: PipefyClient):
|
|
149
|
+
return await client.delete_table(table_id)
|
|
150
|
+
|
|
151
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""AI usage dashboards (agents, automations, credits)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from pipefy_sdk import PipefyClient
|
|
7
|
+
|
|
8
|
+
from pipefy_cli.commands._common import parse_json_object, run_cli_command
|
|
9
|
+
|
|
10
|
+
usage_app = typer.Typer(help="AI and automation usage metrics.", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_date_range(from_iso: str, to_iso: str) -> dict[str, str]:
|
|
14
|
+
return {"from": from_iso.strip(), "to": to_iso.strip()}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@usage_app.command("agents")
|
|
18
|
+
def usage_agents(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
organization: str = typer.Option(
|
|
21
|
+
...,
|
|
22
|
+
"--organization",
|
|
23
|
+
"--org",
|
|
24
|
+
help="Organization UUID (or numeric id resolvable by SDK).",
|
|
25
|
+
),
|
|
26
|
+
date_from: str = typer.Option(
|
|
27
|
+
..., "--from", help="ISO8601 start (filter_date.from)."
|
|
28
|
+
),
|
|
29
|
+
date_to: str = typer.Option(..., "--to", help="ISO8601 end (filter_date.to)."),
|
|
30
|
+
filters: str | None = typer.Option(
|
|
31
|
+
None, "--filters", help="Optional JSON object (FilterParams shape)."
|
|
32
|
+
),
|
|
33
|
+
search: str | None = typer.Option(
|
|
34
|
+
None, "--search", help="Optional free-text search."
|
|
35
|
+
),
|
|
36
|
+
sort: str | None = typer.Option(
|
|
37
|
+
None, "--sort", help="Optional JSON sort criteria object."
|
|
38
|
+
),
|
|
39
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""AI agent usage for an org (``get_agents_usage``)."""
|
|
42
|
+
filt = parse_json_object(filters, "--filters")
|
|
43
|
+
sort_obj = parse_json_object(sort, "--sort")
|
|
44
|
+
|
|
45
|
+
async def factory(client: PipefyClient):
|
|
46
|
+
return await client.get_agents_usage(
|
|
47
|
+
organization,
|
|
48
|
+
_parse_date_range(date_from, date_to),
|
|
49
|
+
filters=filt,
|
|
50
|
+
search=search,
|
|
51
|
+
sort=sort_obj,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
run_cli_command(ctx, json_out, factory)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@usage_app.command("automations")
|
|
58
|
+
def usage_automations(
|
|
59
|
+
ctx: typer.Context,
|
|
60
|
+
organization: str = typer.Option(..., "--organization", "--org"),
|
|
61
|
+
date_from: str = typer.Option(..., "--from"),
|
|
62
|
+
date_to: str = typer.Option(..., "--to"),
|
|
63
|
+
filters: str | None = typer.Option(None, "--filters"),
|
|
64
|
+
search: str | None = typer.Option(None, "--search"),
|
|
65
|
+
sort: str | None = typer.Option(None, "--sort"),
|
|
66
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Automation usage for an org (``get_automations_usage``)."""
|
|
69
|
+
filt = parse_json_object(filters, "--filters")
|
|
70
|
+
sort_obj = parse_json_object(sort, "--sort")
|
|
71
|
+
|
|
72
|
+
async def factory(client: PipefyClient):
|
|
73
|
+
return await client.get_automations_usage(
|
|
74
|
+
organization,
|
|
75
|
+
_parse_date_range(date_from, date_to),
|
|
76
|
+
filters=filt,
|
|
77
|
+
search=search,
|
|
78
|
+
sort=sort_obj,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
run_cli_command(ctx, json_out, factory)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@usage_app.command("credits")
|
|
85
|
+
def usage_credits(
|
|
86
|
+
ctx: typer.Context,
|
|
87
|
+
organization: str = typer.Option(..., "--organization", "--org"),
|
|
88
|
+
period: str = typer.Option(
|
|
89
|
+
"current_month",
|
|
90
|
+
"--period",
|
|
91
|
+
help="current_month | last_month | last_3_months",
|
|
92
|
+
),
|
|
93
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""AI credit usage dashboard (``get_ai_credit_usage``)."""
|
|
96
|
+
|
|
97
|
+
async def factory(client: PipefyClient):
|
|
98
|
+
return await client.get_ai_credit_usage(organization, period)
|
|
99
|
+
|
|
100
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Webhook subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from pipefy_sdk import PipefyClient
|
|
10
|
+
|
|
11
|
+
from pipefy_cli.commands._common import (
|
|
12
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
13
|
+
confirm_destructive,
|
|
14
|
+
parse_json_object,
|
|
15
|
+
resource_id_argument,
|
|
16
|
+
run_cli_command,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
webhook_app = typer.Typer(help="Pipe webhook operations.", no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_actions_json(raw: str) -> list[str]:
|
|
23
|
+
try:
|
|
24
|
+
parsed = json.loads(raw)
|
|
25
|
+
except json.JSONDecodeError as exc:
|
|
26
|
+
raise typer.BadParameter(f"Invalid JSON for --actions: {exc}") from exc
|
|
27
|
+
if not isinstance(parsed, list) or not parsed:
|
|
28
|
+
raise typer.BadParameter("--actions must be a non-empty JSON array of strings.")
|
|
29
|
+
out: list[str] = []
|
|
30
|
+
for item in parsed:
|
|
31
|
+
if not isinstance(item, str) or not item.strip():
|
|
32
|
+
raise typer.BadParameter("Each action must be a non-empty string.")
|
|
33
|
+
out.append(item.strip())
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@webhook_app.command("list")
|
|
38
|
+
def webhook_list(
|
|
39
|
+
ctx: typer.Context,
|
|
40
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
41
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""List webhooks on a pipe."""
|
|
44
|
+
|
|
45
|
+
async def factory(client: PipefyClient):
|
|
46
|
+
return await client.get_webhooks(pipe_id)
|
|
47
|
+
|
|
48
|
+
run_cli_command(ctx, json_out, factory)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@webhook_app.command("create")
|
|
52
|
+
def webhook_create(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
55
|
+
url: str = typer.Option(..., "--url", help="HTTPS callback URL."),
|
|
56
|
+
actions_json: str = typer.Option(
|
|
57
|
+
...,
|
|
58
|
+
"--actions",
|
|
59
|
+
help='JSON array of event action strings, e.g. \'["card.create","card.move"]\'.',
|
|
60
|
+
),
|
|
61
|
+
extra_json: str | None = typer.Option(
|
|
62
|
+
None,
|
|
63
|
+
"--extra",
|
|
64
|
+
help="JSON object of extra CreateWebhookInput fields.",
|
|
65
|
+
),
|
|
66
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Create a webhook."""
|
|
69
|
+
|
|
70
|
+
actions = _parse_actions_json(actions_json)
|
|
71
|
+
extra = parse_json_object(extra_json, "--extra") or {}
|
|
72
|
+
u = url.strip()
|
|
73
|
+
if not u:
|
|
74
|
+
typer.echo("--url must be non-empty.", err=True)
|
|
75
|
+
raise typer.Exit(2)
|
|
76
|
+
|
|
77
|
+
async def factory(client: PipefyClient):
|
|
78
|
+
return await client.create_webhook(pipe_id, u, actions, **extra)
|
|
79
|
+
|
|
80
|
+
run_cli_command(ctx, json_out, factory)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@webhook_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
84
|
+
def webhook_update(
|
|
85
|
+
ctx: typer.Context,
|
|
86
|
+
webhook_id: str = resource_id_argument(help="Webhook id."),
|
|
87
|
+
name: str | None = typer.Option(None, "--name"),
|
|
88
|
+
url: str | None = typer.Option(None, "--url"),
|
|
89
|
+
actions_json: str | None = typer.Option(
|
|
90
|
+
None,
|
|
91
|
+
"--actions",
|
|
92
|
+
help="JSON array of event action strings (non-empty when provided).",
|
|
93
|
+
),
|
|
94
|
+
headers_json: str | None = typer.Option(
|
|
95
|
+
None,
|
|
96
|
+
"--headers",
|
|
97
|
+
help="JSON object of custom HTTP headers.",
|
|
98
|
+
),
|
|
99
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Update a webhook (pass at least one attribute)."""
|
|
102
|
+
|
|
103
|
+
kwargs: dict[str, Any] = {}
|
|
104
|
+
if name is not None:
|
|
105
|
+
if not name.strip():
|
|
106
|
+
raise typer.BadParameter("--name, when provided, must be non-empty.")
|
|
107
|
+
kwargs["name"] = name.strip()
|
|
108
|
+
if url is not None:
|
|
109
|
+
if not url.strip():
|
|
110
|
+
raise typer.BadParameter("--url, when provided, must be non-empty.")
|
|
111
|
+
kwargs["url"] = url.strip()
|
|
112
|
+
if actions_json is not None:
|
|
113
|
+
kwargs["actions"] = _parse_actions_json(actions_json)
|
|
114
|
+
if headers_json is not None:
|
|
115
|
+
kwargs["headers"] = parse_json_object(headers_json, "--headers") or {}
|
|
116
|
+
if not kwargs:
|
|
117
|
+
raise typer.BadParameter(
|
|
118
|
+
"Provide at least one of: --name, --url, --actions, --headers."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def factory(client: PipefyClient):
|
|
122
|
+
return await client.update_webhook(webhook_id, **kwargs)
|
|
123
|
+
|
|
124
|
+
run_cli_command(ctx, json_out, factory)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@webhook_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
128
|
+
def webhook_delete(
|
|
129
|
+
ctx: typer.Context,
|
|
130
|
+
webhook_id: str = resource_id_argument(help="Webhook id."),
|
|
131
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
132
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Delete a webhook permanently."""
|
|
135
|
+
|
|
136
|
+
confirm_destructive(yes=yes, description=f"webhook {webhook_id}")
|
|
137
|
+
|
|
138
|
+
async def factory(client: PipefyClient):
|
|
139
|
+
return await client.delete_webhook(webhook_id)
|
|
140
|
+
|
|
141
|
+
run_cli_command(ctx, json_out, factory)
|