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.
Files changed (40) hide show
  1. pipefy_cli/__init__.py +7 -0
  2. pipefy_cli/_docs.py +3 -0
  3. pipefy_cli/auth.py +225 -0
  4. pipefy_cli/commands/__init__.py +5 -0
  5. pipefy_cli/commands/_common.py +359 -0
  6. pipefy_cli/commands/agent.py +358 -0
  7. pipefy_cli/commands/ai_automation.py +318 -0
  8. pipefy_cli/commands/attachment.py +122 -0
  9. pipefy_cli/commands/audit.py +63 -0
  10. pipefy_cli/commands/auth.py +519 -0
  11. pipefy_cli/commands/automation.py +529 -0
  12. pipefy_cli/commands/card.py +439 -0
  13. pipefy_cli/commands/email.py +169 -0
  14. pipefy_cli/commands/export.py +61 -0
  15. pipefy_cli/commands/field.py +113 -0
  16. pipefy_cli/commands/field_condition.py +201 -0
  17. pipefy_cli/commands/graphql.py +110 -0
  18. pipefy_cli/commands/introspect.py +110 -0
  19. pipefy_cli/commands/label.py +109 -0
  20. pipefy_cli/commands/member.py +126 -0
  21. pipefy_cli/commands/org.py +60 -0
  22. pipefy_cli/commands/phase.py +264 -0
  23. pipefy_cli/commands/pipe.py +167 -0
  24. pipefy_cli/commands/portal.py +871 -0
  25. pipefy_cli/commands/record.py +219 -0
  26. pipefy_cli/commands/relation.py +173 -0
  27. pipefy_cli/commands/report_org.py +241 -0
  28. pipefy_cli/commands/report_pipe.py +290 -0
  29. pipefy_cli/commands/table.py +151 -0
  30. pipefy_cli/commands/usage.py +100 -0
  31. pipefy_cli/commands/webhook.py +141 -0
  32. pipefy_cli/main.py +132 -0
  33. pipefy_cli/output/__init__.py +8 -0
  34. pipefy_cli/output/json_renderer.py +24 -0
  35. pipefy_cli/output/rich_renderer.py +81 -0
  36. pipefy_cli/settings.py +66 -0
  37. pipefy_cli-0.3.0a1.dist-info/METADATA +104 -0
  38. pipefy_cli-0.3.0a1.dist-info/RECORD +40 -0
  39. pipefy_cli-0.3.0a1.dist-info/WHEEL +4 -0
  40. pipefy_cli-0.3.0a1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,219 @@
1
+ """Database table record subcommands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+ from pipefy_sdk import (
9
+ UPDATE_TABLE_RECORD_ALLOWED_FIELD_KEYS,
10
+ UPDATE_TABLE_RECORD_FIELDS_ERROR_MESSAGE,
11
+ PipefyClient,
12
+ )
13
+
14
+ from pipefy_cli.commands._common import (
15
+ ID_POSITIONAL_CONTEXT_SETTINGS,
16
+ confirm_destructive,
17
+ parse_json_object,
18
+ parse_json_value,
19
+ resource_id_argument,
20
+ run_cli_command,
21
+ )
22
+
23
+ record_app = typer.Typer(help="Table record operations.", no_args_is_help=True)
24
+
25
+ _TABLE_RECORDS_FIRST_MAX = 200
26
+
27
+
28
+ def _parse_fields_json(raw: str | None) -> dict[str, Any] | list[dict[str, Any]] | None:
29
+ if raw is None or raw.strip() == "":
30
+ return None
31
+ parsed = parse_json_value(raw, "--fields")
32
+ if isinstance(parsed, dict | list):
33
+ return parsed
34
+ raise typer.BadParameter("--fields must be a JSON object or array")
35
+
36
+
37
+ @record_app.command("find")
38
+ def record_find(
39
+ ctx: typer.Context,
40
+ table_id: str = typer.Option(..., "--table", help="Database table id."),
41
+ filter_json: str | None = typer.Option(
42
+ None,
43
+ "--filter",
44
+ help=(
45
+ 'JSON object: either {"field_id":"...","field_value":"..."} for findRecords, '
46
+ "or omit field_id/field_value to page all records (get_table_records)."
47
+ ),
48
+ ),
49
+ first: int | None = typer.Option(
50
+ None,
51
+ "--first",
52
+ help="Page size (1-200 for listing; optional for field search).",
53
+ ),
54
+ after: str | None = typer.Option(None, "--after", help="Pagination cursor."),
55
+ json_out: bool = typer.Option(False, "--json", "-j"),
56
+ ) -> None:
57
+ """Find records: field match (``find_records``) or list page (``get_table_records``)."""
58
+
59
+ filt = parse_json_object(filter_json, "--filter") if filter_json else {}
60
+ has_field_id = "field_id" in filt
61
+ has_field_value = "field_value" in filt
62
+ if has_field_id != has_field_value:
63
+ raise typer.BadParameter(
64
+ "--filter must include both 'field_id' and 'field_value' as strings, or neither."
65
+ )
66
+ if has_field_id:
67
+ fid = filt["field_id"]
68
+ fval = filt["field_value"]
69
+ if not (isinstance(fid, str) and isinstance(fval, str)):
70
+ raise typer.BadParameter(
71
+ "filter.field_id and filter.field_value must be strings."
72
+ )
73
+ nfirst = first
74
+ if nfirst is not None and (nfirst < 1 or nfirst > _TABLE_RECORDS_FIRST_MAX):
75
+ raise typer.BadParameter(
76
+ f"--first must be between 1 and {_TABLE_RECORDS_FIRST_MAX}."
77
+ )
78
+
79
+ async def factory(client: PipefyClient):
80
+ return await client.find_records(
81
+ table_id,
82
+ fid.strip(),
83
+ fval,
84
+ first=nfirst,
85
+ after=after,
86
+ )
87
+
88
+ run_cli_command(ctx, json_out, factory)
89
+ return
90
+
91
+ nfirst = first if first is not None else 50
92
+ if nfirst < 1 or nfirst > _TABLE_RECORDS_FIRST_MAX:
93
+ raise typer.BadParameter(
94
+ f"--first must be between 1 and {_TABLE_RECORDS_FIRST_MAX} (default 50)."
95
+ )
96
+
97
+ async def factory(client: PipefyClient):
98
+ return await client.get_table_records(table_id, first=nfirst, after=after)
99
+
100
+ run_cli_command(ctx, json_out, factory)
101
+
102
+
103
+ @record_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
104
+ def record_get(
105
+ ctx: typer.Context,
106
+ record_id: str = resource_id_argument(help="Table record id."),
107
+ json_out: bool = typer.Option(False, "--json", "-j"),
108
+ ) -> None:
109
+ """Load one table record by id."""
110
+
111
+ async def factory(client: PipefyClient):
112
+ return await client.get_table_record(record_id)
113
+
114
+ run_cli_command(ctx, json_out, factory)
115
+
116
+
117
+ @record_app.command("create")
118
+ def record_create(
119
+ ctx: typer.Context,
120
+ table_id: str = typer.Option(..., "--table", help="Database table id."),
121
+ fields_json: str | None = typer.Option(
122
+ None,
123
+ "--fields",
124
+ help="JSON object or array of field values for the new record.",
125
+ ),
126
+ title: str | None = typer.Option(None, "--title", "-t"),
127
+ extra_json: str | None = typer.Option(
128
+ None,
129
+ "--extra",
130
+ help="JSON object of extra CreateTableRecordInput keys.",
131
+ ),
132
+ json_out: bool = typer.Option(False, "--json", "-j"),
133
+ ) -> None:
134
+ """Create a table record."""
135
+
136
+ fields = _parse_fields_json(fields_json)
137
+ extra = parse_json_object(extra_json, "--extra") or {}
138
+ payload = fields if fields is not None else {}
139
+ merged: dict[str, Any] = {**extra}
140
+ if title is not None:
141
+ merged["title"] = title
142
+
143
+ async def factory(client: PipefyClient):
144
+ return await client.create_table_record(table_id, payload, **merged)
145
+
146
+ run_cli_command(ctx, json_out, factory, exit_code_2_on_value_error=True)
147
+
148
+
149
+ @record_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
150
+ def record_update(
151
+ ctx: typer.Context,
152
+ record_id: str = resource_id_argument(help="Table record id."),
153
+ fields_json: str | None = typer.Option(
154
+ None,
155
+ "--fields",
156
+ help="JSON object: title, due_date, status_id or statusId (update_table_record).",
157
+ ),
158
+ field_id: str | None = typer.Option(
159
+ None,
160
+ "--field-id",
161
+ help="When set with --value, calls set_table_record_field_value instead.",
162
+ ),
163
+ value_json: str | None = typer.Option(
164
+ None,
165
+ "--value",
166
+ help="JSON value for --field-id (string, number, array, or object).",
167
+ ),
168
+ json_out: bool = typer.Option(False, "--json", "-j"),
169
+ ) -> None:
170
+ """Update record core fields or one custom field."""
171
+
172
+ if field_id is not None:
173
+ if not field_id.strip():
174
+ raise typer.BadParameter("--field-id must be non-empty when provided.")
175
+ if value_json is None:
176
+ raise typer.BadParameter("Provide --value JSON when using --field-id.")
177
+ parsed_val = parse_json_value(value_json, "--value")
178
+
179
+ async def factory_set(client: PipefyClient):
180
+ return await client.set_table_record_field_value(
181
+ record_id, field_id.strip(), parsed_val
182
+ )
183
+
184
+ run_cli_command(ctx, json_out, factory_set, exit_code_2_on_value_error=True)
185
+ return
186
+
187
+ fields = parse_json_object(fields_json, "--fields")
188
+ if not fields:
189
+ raise typer.BadParameter(
190
+ "Provide --fields for title/due_date/status or --field-id and --value."
191
+ )
192
+ if not any(
193
+ key in UPDATE_TABLE_RECORD_ALLOWED_FIELD_KEYS and value is not None
194
+ for key, value in fields.items()
195
+ ):
196
+ typer.echo(UPDATE_TABLE_RECORD_FIELDS_ERROR_MESSAGE, err=True)
197
+ raise typer.Exit(2)
198
+
199
+ async def factory(client: PipefyClient):
200
+ return await client.update_table_record(record_id, fields)
201
+
202
+ run_cli_command(ctx, json_out, factory, exit_code_2_on_value_error=True)
203
+
204
+
205
+ @record_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
206
+ def record_delete(
207
+ ctx: typer.Context,
208
+ record_id: str = resource_id_argument(help="Table record id."),
209
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
210
+ json_out: bool = typer.Option(False, "--json", "-j"),
211
+ ) -> None:
212
+ """Delete a table record permanently."""
213
+
214
+ confirm_destructive(yes=yes, description=f"table record {record_id}")
215
+
216
+ async def factory(client: PipefyClient):
217
+ return await client.delete_table_record(record_id)
218
+
219
+ run_cli_command(ctx, json_out, factory, exit_code_2_on_value_error=True)
@@ -0,0 +1,173 @@
1
+ """Pipe and card relation subcommands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from pipefy_sdk import PipefyClient
7
+
8
+ from pipefy_cli.commands._common import (
9
+ ID_POSITIONAL_CONTEXT_SETTINGS,
10
+ confirm_destructive,
11
+ parse_json_object,
12
+ resource_id_argument,
13
+ run_cli_command,
14
+ )
15
+
16
+ relation_app = typer.Typer(help="Pipe and card relations.", no_args_is_help=True)
17
+ relation_pipe_app = typer.Typer(help="Pipe-to-pipe relations.", no_args_is_help=True)
18
+ relation_card_app = typer.Typer(help="Card-to-card relations.", no_args_is_help=True)
19
+
20
+
21
+ @relation_pipe_app.command("list", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
22
+ def relation_pipe_list(
23
+ ctx: typer.Context,
24
+ pipe_id: str = resource_id_argument(help="Pipe id."),
25
+ json_out: bool = typer.Option(False, "--json", "-j"),
26
+ ) -> None:
27
+ """List pipe relations for a pipe."""
28
+
29
+ async def factory(client: PipefyClient):
30
+ return await client.get_pipe_relations(pipe_id)
31
+
32
+ run_cli_command(ctx, json_out, factory)
33
+
34
+
35
+ @relation_pipe_app.command("create")
36
+ def relation_pipe_create(
37
+ ctx: typer.Context,
38
+ parent_id: str = typer.Option(..., "--parent", help="Parent pipe id."),
39
+ child_id: str = typer.Option(..., "--child", help="Child pipe id."),
40
+ name: str = typer.Option(..., "--name", "-n", help="Relation name."),
41
+ extra_json: str | None = typer.Option(
42
+ None,
43
+ "--extra",
44
+ help="JSON object merged into CreatePipeRelationInput.",
45
+ ),
46
+ json_out: bool = typer.Option(False, "--json", "-j"),
47
+ ) -> None:
48
+ """Create a pipe relation."""
49
+
50
+ nm = name.strip()
51
+ if not nm:
52
+ typer.echo("--name must be non-empty.", err=True)
53
+ raise typer.Exit(2)
54
+ extra = parse_json_object(extra_json, "--extra")
55
+
56
+ async def factory(client: PipefyClient):
57
+ return await client.create_pipe_relation(
58
+ parent_id, child_id, nm, extra_input=extra
59
+ )
60
+
61
+ run_cli_command(ctx, json_out, factory)
62
+
63
+
64
+ @relation_pipe_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
65
+ def relation_pipe_update(
66
+ ctx: typer.Context,
67
+ relation_id: str = resource_id_argument(help="Pipe relation id."),
68
+ name: str = typer.Option(..., "--name", "-n", help="New relation name."),
69
+ extra_json: str | None = typer.Option(
70
+ None,
71
+ "--extra",
72
+ help="JSON object merged into UpdatePipeRelationInput.",
73
+ ),
74
+ json_out: bool = typer.Option(False, "--json", "-j"),
75
+ ) -> None:
76
+ """Update a pipe relation."""
77
+
78
+ nm = name.strip()
79
+ if not nm:
80
+ typer.echo("--name must be non-empty.", err=True)
81
+ raise typer.Exit(2)
82
+ extra = parse_json_object(extra_json, "--extra")
83
+
84
+ async def factory(client: PipefyClient):
85
+ return await client.update_pipe_relation(relation_id, nm, extra_input=extra)
86
+
87
+ run_cli_command(ctx, json_out, factory)
88
+
89
+
90
+ @relation_pipe_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
91
+ def relation_pipe_delete(
92
+ ctx: typer.Context,
93
+ relation_id: str = resource_id_argument(help="Pipe relation id."),
94
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
95
+ json_out: bool = typer.Option(False, "--json", "-j"),
96
+ ) -> None:
97
+ """Delete a pipe relation permanently."""
98
+
99
+ confirm_destructive(yes=yes, description=f"pipe relation {relation_id}")
100
+
101
+ async def factory(client: PipefyClient):
102
+ return await client.delete_pipe_relation(relation_id)
103
+
104
+ run_cli_command(ctx, json_out, factory)
105
+
106
+
107
+ @relation_card_app.command("list", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
108
+ def relation_card_list(
109
+ ctx: typer.Context,
110
+ card_id: str = resource_id_argument(help="Card id."),
111
+ json_out: bool = typer.Option(False, "--json", "-j"),
112
+ ) -> None:
113
+ """List parent and child relations for a card (raw ``get_card_relations`` payload)."""
114
+
115
+ async def factory(client: PipefyClient):
116
+ return await client.get_card_relations(card_id)
117
+
118
+ run_cli_command(ctx, json_out, factory)
119
+
120
+
121
+ @relation_card_app.command("create")
122
+ def relation_card_create(
123
+ ctx: typer.Context,
124
+ parent_id: str = typer.Option(..., "--parent", help="Parent card id."),
125
+ child_id: str = typer.Option(..., "--child", help="Child card id."),
126
+ source_id: str = typer.Option(
127
+ ...,
128
+ "--source",
129
+ help="Pipe relation id from ``relation pipe list``.",
130
+ ),
131
+ extra_json: str | None = typer.Option(
132
+ None,
133
+ "--extra",
134
+ help="JSON object merged into CreateCardRelationInput.",
135
+ ),
136
+ json_out: bool = typer.Option(False, "--json", "-j"),
137
+ ) -> None:
138
+ """Link two cards via an existing pipe relation."""
139
+
140
+ extra = parse_json_object(extra_json, "--extra")
141
+
142
+ async def factory(client: PipefyClient):
143
+ return await client.create_card_relation(
144
+ parent_id, child_id, source_id, extra_input=extra
145
+ )
146
+
147
+ run_cli_command(ctx, json_out, factory)
148
+
149
+
150
+ @relation_card_app.command("delete")
151
+ def relation_card_delete(
152
+ ctx: typer.Context,
153
+ child_id: str = typer.Option(..., "--child", help="Child card id."),
154
+ parent_id: str = typer.Option(..., "--parent", help="Parent card id."),
155
+ source_id: str = typer.Option(..., "--source", help="Pipe relation id."),
156
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
157
+ json_out: bool = typer.Option(False, "--json", "-j"),
158
+ ) -> None:
159
+ """Remove a card relation (Internal API, requires OAuth)."""
160
+
161
+ confirm_destructive(
162
+ yes=yes,
163
+ description=f"card relation (child={child_id}, parent={parent_id}, source={source_id})",
164
+ )
165
+
166
+ async def factory(c: PipefyClient):
167
+ return await c.delete_card_relation(child_id, parent_id, source_id)
168
+
169
+ run_cli_command(ctx, json_out, factory)
170
+
171
+
172
+ relation_app.add_typer(relation_pipe_app, name="pipe")
173
+ relation_app.add_typer(relation_card_app, name="card")
@@ -0,0 +1,241 @@
1
+ """Organization-level reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from pipefy_sdk import PipefyClient
7
+
8
+ from pipefy_cli.commands._common import (
9
+ ID_POSITIONAL_CONTEXT_SETTINGS,
10
+ confirm_destructive,
11
+ parse_json_object,
12
+ parse_json_value,
13
+ prepare_report_cards_filter_cli,
14
+ resource_id_argument,
15
+ run_cli_command,
16
+ run_pipefy_client_coroutine,
17
+ write_export_csv_to_stdout,
18
+ )
19
+
20
+ report_org_app = typer.Typer(help="Organization reports.", no_args_is_help=True)
21
+
22
+
23
+ @report_org_app.command("list")
24
+ def report_org_list(
25
+ ctx: typer.Context,
26
+ organization: str = typer.Option(..., "--organization", "--org"),
27
+ first: int = typer.Option(30, "--first", help="Page size."),
28
+ after: str | None = typer.Option(None, "--after", help="Pagination cursor."),
29
+ json_out: bool = typer.Option(
30
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
31
+ ),
32
+ ) -> None:
33
+ """List organization reports (``get_organization_reports``)."""
34
+
35
+ async def factory(client: PipefyClient):
36
+ return await client.get_organization_reports(
37
+ organization, first=first, after=after
38
+ )
39
+
40
+ run_cli_command(ctx, json_out, factory)
41
+
42
+
43
+ @report_org_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
44
+ def report_org_get(
45
+ ctx: typer.Context,
46
+ report_id: str = resource_id_argument(help="Organization report id."),
47
+ json_out: bool = typer.Option(
48
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
49
+ ),
50
+ ) -> None:
51
+ """Fetch one organization report (``get_organization_report``)."""
52
+
53
+ async def factory(client: PipefyClient):
54
+ return await client.get_organization_report(report_id)
55
+
56
+ run_cli_command(ctx, json_out, factory)
57
+
58
+
59
+ @report_org_app.command("create")
60
+ def report_org_create(
61
+ ctx: typer.Context,
62
+ organization: str = typer.Option(..., "--organization", "--org"),
63
+ name: str = typer.Option(..., "--name", "-n"),
64
+ pipe_ids: str = typer.Option(
65
+ ..., "--pipe-ids", help="JSON array of pipe id strings."
66
+ ),
67
+ fields: str | None = typer.Option(None, "--fields"),
68
+ filter_json: str | None = typer.Option(None, "--filter"),
69
+ json_out: bool = typer.Option(
70
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
71
+ ),
72
+ ) -> None:
73
+ """Create an organization report (``create_organization_report``)."""
74
+ pids = parse_json_value(pipe_ids, "--pipe-ids")
75
+ if not isinstance(pids, list) or not pids:
76
+ raise typer.BadParameter("--pipe-ids must be a non-empty JSON array")
77
+ pipe_list = [str(x) for x in pids]
78
+ fields_list = parse_json_value(fields, "--fields") if fields else None
79
+ if fields_list is not None and not isinstance(fields_list, list):
80
+ raise typer.BadParameter("--fields must be a JSON array")
81
+ filt = prepare_report_cards_filter_cli(parse_json_object(filter_json, "--filter"))
82
+
83
+ async def factory(client: PipefyClient):
84
+ return await client.create_organization_report(
85
+ organization,
86
+ name.strip(),
87
+ pipe_list,
88
+ fields=fields_list,
89
+ filter=filt,
90
+ )
91
+
92
+ run_cli_command(ctx, json_out, factory)
93
+
94
+
95
+ @report_org_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
96
+ def report_org_update(
97
+ ctx: typer.Context,
98
+ report_id: str = resource_id_argument(help="Organization report id."),
99
+ name: str | None = typer.Option(None, "--name", "-n"),
100
+ color: str | None = typer.Option(None, "--color"),
101
+ fields: str | None = typer.Option(None, "--fields"),
102
+ filter_json: str | None = typer.Option(None, "--filter"),
103
+ pipe_ids: str | None = typer.Option(
104
+ None, "--pipe-ids", help="JSON array of pipe ids."
105
+ ),
106
+ json_out: bool = typer.Option(
107
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
108
+ ),
109
+ ) -> None:
110
+ """Update an organization report (``update_organization_report``)."""
111
+ fields_list = parse_json_value(fields, "--fields") if fields else None
112
+ if fields_list is not None and not isinstance(fields_list, list):
113
+ raise typer.BadParameter("--fields must be a JSON array")
114
+ filt = prepare_report_cards_filter_cli(parse_json_object(filter_json, "--filter"))
115
+ pids = parse_json_value(pipe_ids, "--pipe-ids") if pipe_ids else None
116
+ if pids is not None:
117
+ if not isinstance(pids, list):
118
+ raise typer.BadParameter("--pipe-ids must be a JSON array")
119
+ pids = [str(x) for x in pids]
120
+
121
+ async def factory(client: PipefyClient):
122
+ return await client.update_organization_report(
123
+ report_id,
124
+ name=name.strip() if name else None,
125
+ color=color,
126
+ fields=fields_list,
127
+ filter=filt,
128
+ pipe_ids=pids,
129
+ )
130
+
131
+ run_cli_command(ctx, json_out, factory)
132
+
133
+
134
+ @report_org_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
135
+ def report_org_delete(
136
+ ctx: typer.Context,
137
+ report_id: str = resource_id_argument(help="Organization report id."),
138
+ yes: bool = typer.Option(False, "--yes", "-y"),
139
+ json_out: bool = typer.Option(
140
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
141
+ ),
142
+ ) -> None:
143
+ """Delete an organization report (``delete_organization_report``)."""
144
+ confirm_destructive(
145
+ yes=yes, description=f"organization report {report_id}", verb="delete"
146
+ )
147
+
148
+ async def factory(client: PipefyClient):
149
+ return await client.delete_organization_report(report_id)
150
+
151
+ run_cli_command(ctx, json_out, factory)
152
+
153
+
154
+ @report_org_app.command("export")
155
+ def report_org_export(
156
+ ctx: typer.Context,
157
+ organization: str = typer.Option(..., "--organization", "--org"),
158
+ format_: str = typer.Option(
159
+ "json",
160
+ "--format",
161
+ help="json: return export mutation payload; csv: poll and stream download.",
162
+ ),
163
+ organization_report_id: str | None = typer.Option(
164
+ None, "--organization-report-id", help="Optional report id to export."
165
+ ),
166
+ pipe_ids: str | None = typer.Option(
167
+ None, "--pipe-ids", help="Optional JSON array of pipe ids."
168
+ ),
169
+ sort_by: str | None = typer.Option(None, "--sort-by"),
170
+ filter_json: str | None = typer.Option(None, "--filter"),
171
+ columns: str | None = typer.Option(None, "--columns"),
172
+ poll_timeout: float = typer.Option(
173
+ 90.0,
174
+ "--poll-timeout",
175
+ help="Seconds to poll export status before failing (CSV only).",
176
+ ),
177
+ json_out: bool = typer.Option(
178
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
179
+ ),
180
+ ) -> None:
181
+ """Export an organization report (``export_organization_report``)."""
182
+ fmt = format_.strip().lower()
183
+ if fmt not in ("json", "csv"):
184
+ raise typer.BadParameter("--format must be json or csv")
185
+ if fmt == "csv" and json_out:
186
+ raise typer.BadParameter(
187
+ "--json is mutually exclusive with --format csv (CSV is written as raw bytes to stdout)."
188
+ )
189
+ sort_obj = parse_json_object(sort_by, "--sort-by")
190
+ filt = prepare_report_cards_filter_cli(parse_json_object(filter_json, "--filter"))
191
+ cols = parse_json_value(columns, "--columns") if columns else None
192
+ if cols is not None and not isinstance(cols, list):
193
+ raise typer.BadParameter("--columns must be a JSON array")
194
+ pids = parse_json_value(pipe_ids, "--pipe-ids") if pipe_ids else None
195
+ if pids is not None:
196
+ if not isinstance(pids, list):
197
+ raise typer.BadParameter("--pipe-ids must be a JSON array")
198
+ pids = [str(x) for x in pids]
199
+
200
+ if fmt == "json":
201
+
202
+ async def factory(client: PipefyClient):
203
+ return await client.export_organization_report(
204
+ organization,
205
+ organization_report_id=organization_report_id,
206
+ pipe_ids=pids,
207
+ sort_by=sort_obj,
208
+ filter=filt,
209
+ columns=cols,
210
+ )
211
+
212
+ run_cli_command(ctx, json_out, factory)
213
+ return
214
+
215
+ async def csv_factory(client: PipefyClient) -> None:
216
+ start = await client.export_organization_report(
217
+ organization,
218
+ organization_report_id=organization_report_id,
219
+ pipe_ids=pids,
220
+ sort_by=sort_obj,
221
+ filter=filt,
222
+ columns=cols,
223
+ )
224
+ exp = (start.get("exportOrganizationReport") or {}).get(
225
+ "organizationReportExport"
226
+ ) or {}
227
+ export_id = exp.get("id")
228
+ if not export_id:
229
+ typer.echo(
230
+ "Could not read export id from exportOrganizationReport response.",
231
+ err=True,
232
+ )
233
+ raise typer.Exit(1)
234
+ await write_export_csv_to_stdout(
235
+ export_id=str(export_id),
236
+ poll_fetch=client.get_organization_report_export,
237
+ unwrap_status=lambda raw: (raw or {}).get("organizationReportExport") or {},
238
+ poll_timeout_seconds=poll_timeout,
239
+ )
240
+
241
+ run_pipefy_client_coroutine(ctx, csv_factory, value_error_exit_code=1)