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,439 @@
|
|
|
1
|
+
"""Card subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import (
|
|
9
|
+
CardSearch,
|
|
10
|
+
CommentInput,
|
|
11
|
+
DeleteCommentInput,
|
|
12
|
+
PipefyClient,
|
|
13
|
+
UpdateCommentInput,
|
|
14
|
+
copy_card_search,
|
|
15
|
+
)
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from pipefy_cli.commands._common import (
|
|
19
|
+
_CARDS_PAGE_SIZE_MAX,
|
|
20
|
+
_CARDS_PAGE_SIZE_MIN,
|
|
21
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
22
|
+
confirm_destructive,
|
|
23
|
+
format_card_get_transport_query_error,
|
|
24
|
+
parse_json_value,
|
|
25
|
+
resource_id_argument,
|
|
26
|
+
run_cli_command,
|
|
27
|
+
validate_cards_page_size,
|
|
28
|
+
validate_optional_resource_id,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
card_app = typer.Typer(help="Card operations.", no_args_is_help=True)
|
|
32
|
+
comment_app = typer.Typer(help="Card comments.", no_args_is_help=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_card_search_json(raw: str | None) -> CardSearch | None:
|
|
36
|
+
"""Parse ``--search`` into a ``CardSearch`` (unknown keys dropped, MCP-aligned)."""
|
|
37
|
+
if raw is None or raw.strip() == "":
|
|
38
|
+
return None
|
|
39
|
+
parsed = parse_json_value(raw, "--search")
|
|
40
|
+
if not isinstance(parsed, dict):
|
|
41
|
+
raise typer.BadParameter("--search must be a JSON object")
|
|
42
|
+
return copy_card_search(parsed)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_fields_json(raw: str | None) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
46
|
+
if raw is None or raw.strip() == "":
|
|
47
|
+
return None
|
|
48
|
+
parsed = parse_json_value(raw, "--fields")
|
|
49
|
+
if isinstance(parsed, dict | list):
|
|
50
|
+
return parsed
|
|
51
|
+
raise typer.BadParameter("--fields must be a JSON object or array")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _apply_create_card_title_warning(
|
|
55
|
+
result: dict[str, Any], *, requested_title: str | None
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
if not requested_title:
|
|
58
|
+
return result
|
|
59
|
+
card_node = (result.get("createCard") or {}).get("card")
|
|
60
|
+
if not isinstance(card_node, dict):
|
|
61
|
+
return result
|
|
62
|
+
if card_node.get("title") == requested_title:
|
|
63
|
+
return result
|
|
64
|
+
warned = dict(result)
|
|
65
|
+
warned["title_warning"] = (
|
|
66
|
+
"Card created but title was not applied as expected "
|
|
67
|
+
f"(response title={card_node.get('title')!r}, requested={requested_title!r})."
|
|
68
|
+
)
|
|
69
|
+
return warned
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_field_updates_json(raw: str | None) -> list[dict[str, Any]] | None:
|
|
73
|
+
if raw is None or raw.strip() == "":
|
|
74
|
+
return None
|
|
75
|
+
parsed = parse_json_value(raw, "--field-updates")
|
|
76
|
+
if not isinstance(parsed, list):
|
|
77
|
+
raise typer.BadParameter("--field-updates must be a JSON array")
|
|
78
|
+
return parsed
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _split_csv_ids(raw: str | None) -> list[str | int] | None:
|
|
82
|
+
if raw is None or not raw.strip():
|
|
83
|
+
return None
|
|
84
|
+
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
|
85
|
+
if not parts:
|
|
86
|
+
return None
|
|
87
|
+
out: list[str | int] = []
|
|
88
|
+
for p in parts:
|
|
89
|
+
if p.isdigit():
|
|
90
|
+
out.append(int(p))
|
|
91
|
+
else:
|
|
92
|
+
out.append(p)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@card_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
97
|
+
def card_get(
|
|
98
|
+
ctx: typer.Context,
|
|
99
|
+
card_id: str = resource_id_argument(help="Card id."),
|
|
100
|
+
json_out: bool = typer.Option(
|
|
101
|
+
False,
|
|
102
|
+
"--json",
|
|
103
|
+
"-j",
|
|
104
|
+
help="Print machine-readable JSON to stdout.",
|
|
105
|
+
),
|
|
106
|
+
include_fields: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--include-fields",
|
|
109
|
+
help="Include custom field name/value pairs on the card.",
|
|
110
|
+
),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Fetch a card by id."""
|
|
113
|
+
|
|
114
|
+
async def factory(client: PipefyClient):
|
|
115
|
+
return await client.get_card(card_id, include_fields=include_fields)
|
|
116
|
+
|
|
117
|
+
run_cli_command(
|
|
118
|
+
ctx,
|
|
119
|
+
json_out,
|
|
120
|
+
factory,
|
|
121
|
+
format_transport_query_error=format_card_get_transport_query_error,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@card_app.command("list")
|
|
126
|
+
def card_list(
|
|
127
|
+
ctx: typer.Context,
|
|
128
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id whose cards to load."),
|
|
129
|
+
title: str | None = typer.Option(
|
|
130
|
+
None,
|
|
131
|
+
"--title",
|
|
132
|
+
"-t",
|
|
133
|
+
help="Filter by title substring (merged into search; same shortcut as MCP get_cards).",
|
|
134
|
+
),
|
|
135
|
+
search_json: str | None = typer.Option(
|
|
136
|
+
None,
|
|
137
|
+
"--search",
|
|
138
|
+
help="Optional JSON object: CardSearch keys (title, assignee_ids, label_ids, ignore_ids, include_done, inbox_emails_read). Unknown keys are ignored.",
|
|
139
|
+
),
|
|
140
|
+
include_fields: bool = typer.Option(
|
|
141
|
+
False,
|
|
142
|
+
"--include-fields",
|
|
143
|
+
help="Include each card's custom fields (name, value).",
|
|
144
|
+
),
|
|
145
|
+
first: int | None = typer.Option(
|
|
146
|
+
None,
|
|
147
|
+
"--first",
|
|
148
|
+
help=f"Max cards per page ({_CARDS_PAGE_SIZE_MIN}-{_CARDS_PAGE_SIZE_MAX}).",
|
|
149
|
+
),
|
|
150
|
+
after: str | None = typer.Option(
|
|
151
|
+
None,
|
|
152
|
+
"--after",
|
|
153
|
+
help="Pagination cursor (pageInfo.endCursor from a previous page).",
|
|
154
|
+
),
|
|
155
|
+
json_out: bool = typer.Option(
|
|
156
|
+
False,
|
|
157
|
+
"--json",
|
|
158
|
+
"-j",
|
|
159
|
+
help="Print machine-readable JSON to stdout.",
|
|
160
|
+
),
|
|
161
|
+
) -> None:
|
|
162
|
+
"""List cards in a pipe (get_cards): browse, filter, and paginate.
|
|
163
|
+
|
|
164
|
+
Use ``card find`` when matching a single custom field value (find_cards).
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
search_from_json = _parse_card_search_json(search_json)
|
|
168
|
+
merged: CardSearch = dict(search_from_json) if search_from_json is not None else {}
|
|
169
|
+
if title is not None and title.strip():
|
|
170
|
+
merged["title"] = title.strip()
|
|
171
|
+
effective_search: CardSearch | None = merged if merged else None
|
|
172
|
+
first_validated = validate_cards_page_size(first)
|
|
173
|
+
|
|
174
|
+
async def factory(client: PipefyClient):
|
|
175
|
+
return await client.get_cards(
|
|
176
|
+
pipe_id,
|
|
177
|
+
effective_search,
|
|
178
|
+
include_fields=include_fields,
|
|
179
|
+
first=first_validated,
|
|
180
|
+
after=after,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
run_cli_command(ctx, json_out, factory)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@card_app.command("find")
|
|
187
|
+
def card_find(
|
|
188
|
+
ctx: typer.Context,
|
|
189
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id to search in."),
|
|
190
|
+
field_id: str = typer.Option(
|
|
191
|
+
...,
|
|
192
|
+
"--field",
|
|
193
|
+
help="Field id (slug) from start form or phase fields.",
|
|
194
|
+
),
|
|
195
|
+
field_value: str = typer.Option(
|
|
196
|
+
...,
|
|
197
|
+
"--value",
|
|
198
|
+
help="Value to match for that field.",
|
|
199
|
+
),
|
|
200
|
+
include_fields: bool = typer.Option(
|
|
201
|
+
False,
|
|
202
|
+
"--include-fields",
|
|
203
|
+
help="Include each card's custom fields.",
|
|
204
|
+
),
|
|
205
|
+
first: int | None = typer.Option(
|
|
206
|
+
None,
|
|
207
|
+
"--first",
|
|
208
|
+
help="Max cards per page.",
|
|
209
|
+
),
|
|
210
|
+
after: str | None = typer.Option(
|
|
211
|
+
None,
|
|
212
|
+
"--after",
|
|
213
|
+
help="Pagination cursor (pageInfo.endCursor).",
|
|
214
|
+
),
|
|
215
|
+
json_out: bool = typer.Option(
|
|
216
|
+
False,
|
|
217
|
+
"--json",
|
|
218
|
+
"-j",
|
|
219
|
+
help="Print machine-readable JSON to stdout.",
|
|
220
|
+
),
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Find cards in a pipe where a custom field equals a value."""
|
|
223
|
+
|
|
224
|
+
async def factory(client: PipefyClient):
|
|
225
|
+
return await client.find_cards(
|
|
226
|
+
pipe_id,
|
|
227
|
+
field_id,
|
|
228
|
+
field_value,
|
|
229
|
+
include_fields=include_fields,
|
|
230
|
+
first=first,
|
|
231
|
+
after=after,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
run_cli_command(ctx, json_out, factory)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@card_app.command("create", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
238
|
+
def card_create(
|
|
239
|
+
ctx: typer.Context,
|
|
240
|
+
pipe_id: str = resource_id_argument(help="Pipe id."),
|
|
241
|
+
title: str | None = typer.Option(
|
|
242
|
+
None,
|
|
243
|
+
"--title",
|
|
244
|
+
"-t",
|
|
245
|
+
help="Optional title (sent on CreateCardInput when set).",
|
|
246
|
+
),
|
|
247
|
+
phase_id: str | None = typer.Option(
|
|
248
|
+
None,
|
|
249
|
+
"--phase-id",
|
|
250
|
+
help=(
|
|
251
|
+
"Target phase id (CreateCardInput.phase_id). "
|
|
252
|
+
"Creates the card in that phase instead of the start form."
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
fields_json: str | None = typer.Option(
|
|
256
|
+
None,
|
|
257
|
+
"--fields",
|
|
258
|
+
help="JSON object or array: field values for createCard.",
|
|
259
|
+
),
|
|
260
|
+
json_out: bool = typer.Option(
|
|
261
|
+
False,
|
|
262
|
+
"--json",
|
|
263
|
+
"-j",
|
|
264
|
+
help="Print machine-readable JSON to stdout.",
|
|
265
|
+
),
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Create a card in a pipe (start-form or phase fields via --fields JSON when required)."""
|
|
268
|
+
|
|
269
|
+
fields = _parse_fields_json(fields_json)
|
|
270
|
+
phase_id = validate_optional_resource_id(phase_id, "phase_id")
|
|
271
|
+
|
|
272
|
+
async def factory(client: PipefyClient):
|
|
273
|
+
payload = fields if fields is not None else {}
|
|
274
|
+
create_kwargs: dict[str, Any] = {}
|
|
275
|
+
if phase_id is not None:
|
|
276
|
+
create_kwargs["phase_id"] = phase_id
|
|
277
|
+
if title:
|
|
278
|
+
create_kwargs["title"] = title
|
|
279
|
+
result = await client.create_card(pipe_id, payload, **create_kwargs)
|
|
280
|
+
return _apply_create_card_title_warning(result, requested_title=title)
|
|
281
|
+
|
|
282
|
+
run_cli_command(ctx, json_out, factory)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@card_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
286
|
+
def card_update(
|
|
287
|
+
ctx: typer.Context,
|
|
288
|
+
card_id: str = resource_id_argument(help="Card id."),
|
|
289
|
+
title: str | None = typer.Option(None, "--title", "-t"),
|
|
290
|
+
due_date: str | None = typer.Option(None, "--due-date"),
|
|
291
|
+
assignee_ids: str | None = typer.Option(
|
|
292
|
+
None,
|
|
293
|
+
"--assignee-ids",
|
|
294
|
+
help="Comma-separated assignee user ids.",
|
|
295
|
+
),
|
|
296
|
+
label_ids: str | None = typer.Option(
|
|
297
|
+
None,
|
|
298
|
+
"--label-ids",
|
|
299
|
+
help="Comma-separated label ids.",
|
|
300
|
+
),
|
|
301
|
+
field_updates_json: str | None = typer.Option(
|
|
302
|
+
None,
|
|
303
|
+
"--field-updates",
|
|
304
|
+
help=(
|
|
305
|
+
"JSON array of field update objects for updateFieldsValues. "
|
|
306
|
+
'Each object: {"field_id" (or "fieldId"): "<slug>", "value": <v>}.'
|
|
307
|
+
),
|
|
308
|
+
),
|
|
309
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Update a card (title, assignees, labels, due date, and/or field updates)."""
|
|
312
|
+
|
|
313
|
+
field_updates = _parse_field_updates_json(field_updates_json)
|
|
314
|
+
|
|
315
|
+
async def factory(client: PipefyClient):
|
|
316
|
+
return await client.update_card(
|
|
317
|
+
card_id,
|
|
318
|
+
title=title,
|
|
319
|
+
assignee_ids=_split_csv_ids(assignee_ids),
|
|
320
|
+
label_ids=_split_csv_ids(label_ids),
|
|
321
|
+
due_date=due_date,
|
|
322
|
+
field_updates=field_updates,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
run_cli_command(ctx, json_out, factory)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@card_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
329
|
+
def card_delete(
|
|
330
|
+
ctx: typer.Context,
|
|
331
|
+
card_id: str = resource_id_argument(help="Card id."),
|
|
332
|
+
yes: bool = typer.Option(
|
|
333
|
+
False,
|
|
334
|
+
"--yes",
|
|
335
|
+
"-y",
|
|
336
|
+
help="Skip confirmation prompt.",
|
|
337
|
+
),
|
|
338
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Delete a card permanently."""
|
|
341
|
+
|
|
342
|
+
confirm_destructive(yes=yes, description=f"card {card_id}")
|
|
343
|
+
|
|
344
|
+
async def factory(client: PipefyClient):
|
|
345
|
+
return await client.delete_card(card_id)
|
|
346
|
+
|
|
347
|
+
run_cli_command(ctx, json_out, factory)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@card_app.command("move", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
351
|
+
def card_move(
|
|
352
|
+
ctx: typer.Context,
|
|
353
|
+
card_id: str = resource_id_argument(help="Card id."),
|
|
354
|
+
phase_id: str = typer.Option(
|
|
355
|
+
...,
|
|
356
|
+
"--phase",
|
|
357
|
+
help="Destination phase id.",
|
|
358
|
+
),
|
|
359
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Move a card to another phase."""
|
|
362
|
+
|
|
363
|
+
async def factory(client: PipefyClient):
|
|
364
|
+
return await client.move_card_to_phase(card_id, phase_id)
|
|
365
|
+
|
|
366
|
+
run_cli_command(ctx, json_out, factory)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@comment_app.command("add", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
370
|
+
def card_comment_add(
|
|
371
|
+
ctx: typer.Context,
|
|
372
|
+
card_id: str = resource_id_argument(help="Card id."),
|
|
373
|
+
text: str = typer.Argument(..., help="Comment body (1-1000 characters)."),
|
|
374
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Add a text comment to a card."""
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
validated = CommentInput(card_id=card_id, text=text)
|
|
380
|
+
except ValidationError as exc:
|
|
381
|
+
typer.echo(str(exc), err=True)
|
|
382
|
+
raise typer.Exit(2) from exc
|
|
383
|
+
|
|
384
|
+
async def factory(client: PipefyClient):
|
|
385
|
+
return await client.add_card_comment(validated.card_id, validated.text)
|
|
386
|
+
|
|
387
|
+
run_cli_command(ctx, json_out, factory)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@comment_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
391
|
+
def card_comment_update(
|
|
392
|
+
ctx: typer.Context,
|
|
393
|
+
comment_id: str = resource_id_argument(help="Comment id."),
|
|
394
|
+
text: str = typer.Argument(..., help="New comment text."),
|
|
395
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Update an existing comment by id."""
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
validated = UpdateCommentInput(comment_id=comment_id, text=text)
|
|
401
|
+
except ValidationError as exc:
|
|
402
|
+
typer.echo(str(exc), err=True)
|
|
403
|
+
raise typer.Exit(2) from exc
|
|
404
|
+
|
|
405
|
+
async def factory(client: PipefyClient):
|
|
406
|
+
return await client.update_comment(validated.comment_id, validated.text)
|
|
407
|
+
|
|
408
|
+
run_cli_command(ctx, json_out, factory)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@comment_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
412
|
+
def card_comment_delete(
|
|
413
|
+
ctx: typer.Context,
|
|
414
|
+
comment_id: str = resource_id_argument(help="Comment id."),
|
|
415
|
+
yes: bool = typer.Option(
|
|
416
|
+
False,
|
|
417
|
+
"--yes",
|
|
418
|
+
"-y",
|
|
419
|
+
help="Skip confirmation prompt.",
|
|
420
|
+
),
|
|
421
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Delete a comment by id."""
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
validated = DeleteCommentInput(comment_id=comment_id)
|
|
427
|
+
except ValidationError as exc:
|
|
428
|
+
typer.echo(str(exc), err=True)
|
|
429
|
+
raise typer.Exit(2) from exc
|
|
430
|
+
|
|
431
|
+
confirm_destructive(yes=yes, description=f"comment {validated.comment_id}")
|
|
432
|
+
|
|
433
|
+
async def factory(client: PipefyClient):
|
|
434
|
+
return await client.delete_comment(validated.comment_id)
|
|
435
|
+
|
|
436
|
+
run_cli_command(ctx, json_out, factory)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
card_app.add_typer(comment_app, name="comment")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Card inbox emails and org email templates."""
|
|
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
|
+
email_app = typer.Typer(help="Inbox emails and email templates.", no_args_is_help=True)
|
|
11
|
+
inbox_app = typer.Typer(help="Card inbox (sent/received).", no_args_is_help=True)
|
|
12
|
+
template_app = typer.Typer(
|
|
13
|
+
help="Email templates bound to a pipe or table (repo).", no_args_is_help=True
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@inbox_app.command("list")
|
|
18
|
+
def email_inbox_list(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
card_id: str = typer.Option(..., "--card", help="Card id with inbox enabled."),
|
|
21
|
+
email_type: str | None = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--type",
|
|
24
|
+
help="Optional filter: sent or received.",
|
|
25
|
+
),
|
|
26
|
+
json_out: bool = typer.Option(
|
|
27
|
+
False,
|
|
28
|
+
"--json",
|
|
29
|
+
"-j",
|
|
30
|
+
help="Print machine-readable JSON to stdout.",
|
|
31
|
+
),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""List inbox emails for a card (``get_card_inbox_emails``)."""
|
|
34
|
+
|
|
35
|
+
async def factory(client: PipefyClient):
|
|
36
|
+
return await client.get_card_inbox_emails(card_id, email_type=email_type)
|
|
37
|
+
|
|
38
|
+
run_cli_command(ctx, json_out, factory)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@inbox_app.command("send")
|
|
42
|
+
def email_inbox_send(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
card_id: str = typer.Option(..., "--card", help="Card id."),
|
|
45
|
+
to: str = typer.Option(
|
|
46
|
+
...,
|
|
47
|
+
"--to",
|
|
48
|
+
help="Recipient emails (comma-separated).",
|
|
49
|
+
),
|
|
50
|
+
subject: str = typer.Option(..., "--subject", "-s", help="Email subject."),
|
|
51
|
+
body: str = typer.Option(..., "--body", "-b", help="Plain-text body."),
|
|
52
|
+
from_email: str = typer.Option(
|
|
53
|
+
...,
|
|
54
|
+
"--from-email",
|
|
55
|
+
help="Sender email address (required by API).",
|
|
56
|
+
),
|
|
57
|
+
extra: str | None = typer.Option(
|
|
58
|
+
None,
|
|
59
|
+
"--extra",
|
|
60
|
+
help="Optional JSON object: extra CreateAndSendInboxEmailInput fields.",
|
|
61
|
+
),
|
|
62
|
+
json_out: bool = typer.Option(
|
|
63
|
+
False,
|
|
64
|
+
"--json",
|
|
65
|
+
"-j",
|
|
66
|
+
help="Print machine-readable JSON to stdout.",
|
|
67
|
+
),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Send an inbox email from a card (``send_inbox_email``)."""
|
|
70
|
+
recipients = [e.strip() for e in to.split(",") if e.strip()]
|
|
71
|
+
if not recipients:
|
|
72
|
+
raise typer.BadParameter("--to must list at least one email.")
|
|
73
|
+
extra_obj = parse_json_object(extra, "--extra") or {}
|
|
74
|
+
|
|
75
|
+
async def factory(client: PipefyClient):
|
|
76
|
+
return await client.send_inbox_email(
|
|
77
|
+
card_id,
|
|
78
|
+
recipients,
|
|
79
|
+
subject,
|
|
80
|
+
body,
|
|
81
|
+
from_=from_email,
|
|
82
|
+
**extra_obj,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
run_cli_command(ctx, json_out, factory)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@template_app.command("list")
|
|
89
|
+
def email_template_list(
|
|
90
|
+
ctx: typer.Context,
|
|
91
|
+
repo_id: str = typer.Option(
|
|
92
|
+
...,
|
|
93
|
+
"--repo",
|
|
94
|
+
help="Pipe or table id whose templates to list.",
|
|
95
|
+
),
|
|
96
|
+
name: str | None = typer.Option(
|
|
97
|
+
None,
|
|
98
|
+
"--name",
|
|
99
|
+
help="Optional substring filter on template name.",
|
|
100
|
+
),
|
|
101
|
+
first: int = typer.Option(50, "--first", help="Max templates to return."),
|
|
102
|
+
json_out: bool = typer.Option(
|
|
103
|
+
False,
|
|
104
|
+
"--json",
|
|
105
|
+
"-j",
|
|
106
|
+
help="Print machine-readable JSON to stdout.",
|
|
107
|
+
),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""List email templates for a repo (``get_email_templates``)."""
|
|
110
|
+
|
|
111
|
+
async def factory(client: PipefyClient):
|
|
112
|
+
return await client.get_email_templates(
|
|
113
|
+
repo_id, filter_by_name=name, first=first
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
run_cli_command(ctx, json_out, factory)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@template_app.command("send")
|
|
120
|
+
def email_template_send(
|
|
121
|
+
ctx: typer.Context,
|
|
122
|
+
card_id: str = typer.Option(..., "--card", help="Card id (inbox sender context)."),
|
|
123
|
+
template_id: str = typer.Option(
|
|
124
|
+
...,
|
|
125
|
+
"--template",
|
|
126
|
+
help="Email template id.",
|
|
127
|
+
),
|
|
128
|
+
to: str | None = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--to",
|
|
131
|
+
help="Optional comma-separated override recipients.",
|
|
132
|
+
),
|
|
133
|
+
from_email: str | None = typer.Option(
|
|
134
|
+
None,
|
|
135
|
+
"--from-email",
|
|
136
|
+
help="Optional sender override.",
|
|
137
|
+
),
|
|
138
|
+
extra: str | None = typer.Option(
|
|
139
|
+
None,
|
|
140
|
+
"--extra",
|
|
141
|
+
help="Optional JSON object: extra send fields (cc, bcc, repoId, etc.).",
|
|
142
|
+
),
|
|
143
|
+
json_out: bool = typer.Option(
|
|
144
|
+
False,
|
|
145
|
+
"--json",
|
|
146
|
+
"-j",
|
|
147
|
+
help="Print machine-readable JSON to stdout.",
|
|
148
|
+
),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Send using an email template (``send_email_with_template``)."""
|
|
151
|
+
to_list: list[str] | None = None
|
|
152
|
+
if to is not None and to.strip():
|
|
153
|
+
to_list = [e.strip() for e in to.split(",") if e.strip()]
|
|
154
|
+
extra_obj = parse_json_object(extra, "--extra") or {}
|
|
155
|
+
|
|
156
|
+
async def factory(client: PipefyClient):
|
|
157
|
+
return await client.send_email_with_template(
|
|
158
|
+
card_id,
|
|
159
|
+
template_id,
|
|
160
|
+
to=to_list,
|
|
161
|
+
from_=from_email,
|
|
162
|
+
**extra_obj,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
run_cli_command(ctx, json_out, factory)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
email_app.add_typer(inbox_app, name="inbox")
|
|
169
|
+
email_app.add_typer(template_app, name="template")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Automation job exports."""
|
|
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
|
+
resource_id_argument,
|
|
11
|
+
run_cli_command,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export_app = typer.Typer(
|
|
15
|
+
help="Bulk exports (automation jobs).",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@export_app.command("automation-jobs")
|
|
21
|
+
def export_automation_jobs(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
organization: str = typer.Option(
|
|
24
|
+
..., "--organization", "--org", help="Organization id."
|
|
25
|
+
),
|
|
26
|
+
period: str = typer.Option(
|
|
27
|
+
...,
|
|
28
|
+
"--period",
|
|
29
|
+
help="current_month | last_month | last_3_months",
|
|
30
|
+
),
|
|
31
|
+
json_out: bool = typer.Option(
|
|
32
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
33
|
+
),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Queue automation jobs export (``export_automation_jobs``)."""
|
|
36
|
+
|
|
37
|
+
async def factory(client: PipefyClient):
|
|
38
|
+
return await client.export_automation_jobs(organization, period)
|
|
39
|
+
|
|
40
|
+
run_cli_command(ctx, json_out, factory)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@export_app.command(
|
|
44
|
+
"automation-jobs-csv", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS
|
|
45
|
+
)
|
|
46
|
+
def export_automation_jobs_csv(
|
|
47
|
+
ctx: typer.Context,
|
|
48
|
+
export_id: str = resource_id_argument(help="Export id after status is finished."),
|
|
49
|
+
json_out: bool = typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--json",
|
|
52
|
+
"-j",
|
|
53
|
+
help="Print JSON envelope with CSV text; default is Rich table output.",
|
|
54
|
+
),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Download finished export as CSV text (``get_automation_jobs_export_csv``)."""
|
|
57
|
+
|
|
58
|
+
async def factory(client: PipefyClient):
|
|
59
|
+
return await client.get_automation_jobs_export_csv(export_id)
|
|
60
|
+
|
|
61
|
+
run_cli_command(ctx, json_out, factory)
|