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,871 @@
1
+ """Portal subcommands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+ from pipefy_sdk import (
9
+ CreatePortalElementInput,
10
+ PipefyClient,
11
+ UpdatePortalElementInput,
12
+ )
13
+ from pydantic import ValidationError
14
+
15
+ from pipefy_cli.commands._common import (
16
+ ID_POSITIONAL_CONTEXT_SETTINGS,
17
+ confirm_destructive,
18
+ parse_json_object,
19
+ parse_json_value,
20
+ resource_id_argument,
21
+ run_cli_command,
22
+ )
23
+
24
+ portal_app = typer.Typer(help="Portal operations.", no_args_is_help=True)
25
+ page_app = typer.Typer(help="Portal page operations.", no_args_is_help=True)
26
+ layout_app = typer.Typer(help="Portal page layout.", no_args_is_help=True)
27
+ element_app = typer.Typer(
28
+ help="Portal page elements (widgets / tools in the Pipefy UI).",
29
+ no_args_is_help=True,
30
+ )
31
+ sub_portal_app = typer.Typer(
32
+ help="Sub-portal create, attach, publish, and delete.",
33
+ no_args_is_help=True,
34
+ )
35
+
36
+
37
+ def _require_non_empty_portal_uuid(portal_uuid: str) -> str:
38
+ """Reject blank portal UUIDs before SDK calls."""
39
+ if not portal_uuid.strip():
40
+ raise typer.BadParameter("Portal UUID must be non-empty.")
41
+ return portal_uuid.strip()
42
+
43
+
44
+ def _reject_blank_optional_string(value: str | None, flag: str) -> None:
45
+ """Reject whitespace-only optional string flags on update."""
46
+ if value is not None and not value.strip():
47
+ raise typer.BadParameter(f"{flag}, when provided, must be non-empty.")
48
+
49
+
50
+ def _validate_sort_page_id_item(value: object) -> str:
51
+ """Validate one page UUID from ``--page-ids`` or ``--ids-json`` before a sort write."""
52
+ if isinstance(value, bool) or value is None:
53
+ raise typer.BadParameter(
54
+ "Each page UUID must be a non-empty string or positive integer."
55
+ )
56
+ if isinstance(value, (dict, list)):
57
+ raise typer.BadParameter(
58
+ "Each page UUID must be a non-empty string or positive integer."
59
+ )
60
+ if not isinstance(value, (str, int)):
61
+ raise typer.BadParameter(
62
+ "Each page UUID must be a non-empty string or positive integer."
63
+ )
64
+ cleaned = str(value).strip() if isinstance(value, int) else value.strip()
65
+ if not cleaned:
66
+ raise typer.BadParameter(
67
+ "Each page UUID must be a non-empty string or positive integer."
68
+ )
69
+ if cleaned.startswith("-") and cleaned[1:].isdigit():
70
+ raise typer.BadParameter("Each page UUID must be a positive integer.")
71
+ if cleaned.isdigit() and int(cleaned) <= 0:
72
+ raise typer.BadParameter("Each page UUID must be a positive integer.")
73
+ return cleaned
74
+
75
+
76
+ def _reject_duplicate_sort_page_ids(page_ids: list[str]) -> None:
77
+ """Reject duplicate page identifiers before ``sortPages``."""
78
+ if len(set(page_ids)) != len(page_ids):
79
+ raise typer.BadParameter("Page UUID list must not contain duplicates.")
80
+
81
+
82
+ def _parse_page_ids_for_sort(
83
+ page_ids_csv: str | None,
84
+ ids_json: str | None,
85
+ ) -> list[str]:
86
+ """Resolve ordered page UUIDs from ``--page-ids`` or ``--ids-json``."""
87
+ if page_ids_csv is not None and page_ids_csv.strip():
88
+ parts = [p.strip() for p in page_ids_csv.split(",") if p.strip()]
89
+ if not parts:
90
+ raise typer.BadParameter("--page-ids must list at least one page UUID.")
91
+ ordered = [_validate_sort_page_id_item(part) for part in parts]
92
+ _reject_duplicate_sort_page_ids(ordered)
93
+ return ordered
94
+ if ids_json is not None and ids_json.strip():
95
+ parsed = parse_json_value(ids_json, "--ids-json")
96
+ if not isinstance(parsed, list):
97
+ raise typer.BadParameter("--ids-json must be a JSON array of page UUIDs.")
98
+ if not parsed:
99
+ raise typer.BadParameter("--ids-json must list at least one page UUID.")
100
+ ordered = [_validate_sort_page_id_item(item) for item in parsed]
101
+ _reject_duplicate_sort_page_ids(ordered)
102
+ return ordered
103
+ raise typer.BadParameter(
104
+ "Provide --page-ids or --ids-json with at least one page UUID."
105
+ )
106
+
107
+
108
+ def _parse_required_metadata_json(raw: str | None, option_name: str) -> dict[str, Any]:
109
+ """Parse a required JSON object for element metadata."""
110
+ metadata_obj = parse_json_object(raw, option_name)
111
+ if metadata_obj is None:
112
+ raise typer.BadParameter(f"{option_name} is required.")
113
+ return metadata_obj
114
+
115
+
116
+ def _parse_optional_data_sources_json(raw: str | None) -> list[dict[str, Any]] | None:
117
+ """Parse optional ``--data-sources`` as a JSON array of objects."""
118
+ if raw is None or not raw.strip():
119
+ return None
120
+ parsed = parse_json_value(raw, "--data-sources")
121
+ if not isinstance(parsed, list):
122
+ raise typer.BadParameter("--data-sources must be a JSON array.")
123
+ return parsed
124
+
125
+
126
+ def _portal_element_create_kwargs(
127
+ validated: CreatePortalElementInput,
128
+ ) -> dict[str, Any]:
129
+ """Build kwargs for ``PipefyClient.create_portal_element`` after validation."""
130
+ kwargs: dict[str, Any] = {
131
+ "type": validated.type,
132
+ "metadata": validated.metadata,
133
+ "data_sources": validated.data_sources,
134
+ }
135
+ if validated.element_id is not None:
136
+ kwargs["element_id"] = validated.element_id
137
+ if validated.editable is not None:
138
+ kwargs["editable"] = validated.editable
139
+ if validated.layout is not None:
140
+ kwargs["layout"] = validated.layout
141
+ return kwargs
142
+
143
+
144
+ def _portal_element_update_kwargs(
145
+ validated: UpdatePortalElementInput,
146
+ ) -> dict[str, Any]:
147
+ """Build kwargs for ``PipefyClient.update_portal_element`` after validation."""
148
+ kwargs: dict[str, Any] = {
149
+ "type": validated.type,
150
+ "metadata": validated.metadata,
151
+ "data_sources": validated.data_sources,
152
+ }
153
+ if validated.editable is not None:
154
+ kwargs["editable"] = validated.editable
155
+ return kwargs
156
+
157
+
158
+ @portal_app.command("list")
159
+ def portal_list(
160
+ ctx: typer.Context,
161
+ organization_uuid: str = typer.Option(
162
+ ...,
163
+ "--organization-uuid",
164
+ help="Organization UUID, or numeric organization id (string).",
165
+ ),
166
+ search_term: str | None = typer.Option(
167
+ None,
168
+ "--search-term",
169
+ help="Optional portal name filter.",
170
+ ),
171
+ json_out: bool = typer.Option(
172
+ False,
173
+ "--json",
174
+ "-j",
175
+ help="Print machine-readable JSON to stdout.",
176
+ ),
177
+ ) -> None:
178
+ """List portals for an organization."""
179
+
180
+ async def factory(client: PipefyClient):
181
+ return await client.list_portals(organization_uuid, search_term=search_term)
182
+
183
+ run_cli_command(ctx, json_out, factory)
184
+
185
+
186
+ @portal_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
187
+ def portal_get(
188
+ ctx: typer.Context,
189
+ portal_uuid: str = resource_id_argument(help="Portal UUID."),
190
+ json_out: bool = typer.Option(
191
+ False,
192
+ "--json",
193
+ "-j",
194
+ help="Print machine-readable JSON to stdout.",
195
+ ),
196
+ ) -> None:
197
+ """Fetch a portal by UUID."""
198
+
199
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
200
+
201
+ async def factory(client: PipefyClient):
202
+ return await client.get_portal(portal_uuid)
203
+
204
+ run_cli_command(ctx, json_out, factory)
205
+
206
+
207
+ @portal_app.command("create")
208
+ def portal_create(
209
+ ctx: typer.Context,
210
+ organization_uuid: str = typer.Option(
211
+ ...,
212
+ "--organization-uuid",
213
+ help="Organization UUID, or numeric organization id (string).",
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
+ """Create or fetch the organization's main portal (idempotent)."""
223
+
224
+ async def factory(client: PipefyClient):
225
+ return await client.create_portal(organization_uuid)
226
+
227
+ run_cli_command(ctx, json_out, factory)
228
+
229
+
230
+ @portal_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
231
+ def portal_update(
232
+ ctx: typer.Context,
233
+ portal_uuid: str = resource_id_argument(help="Portal UUID."),
234
+ name: str | None = typer.Option(None, "--name", help="Portal display name."),
235
+ visibility: str | None = typer.Option(
236
+ None,
237
+ "--visibility",
238
+ help="Portal visibility: internal, private, or public.",
239
+ ),
240
+ color: str | None = typer.Option(None, "--color", help="Theme color."),
241
+ icon: str | None = typer.Option(None, "--icon", help="Icon identifier."),
242
+ display_pipefy_header: bool | None = typer.Option(
243
+ None,
244
+ "--display-pipefy-header/--no-display-pipefy-header",
245
+ help="Show or hide the Pipefy header.",
246
+ ),
247
+ json_out: bool = typer.Option(
248
+ False,
249
+ "--json",
250
+ "-j",
251
+ help="Print machine-readable JSON to stdout.",
252
+ ),
253
+ ) -> None:
254
+ """Update portal metadata (pass at least one attribute)."""
255
+
256
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
257
+
258
+ if all(x is None for x in (name, visibility, color, icon, display_pipefy_header)):
259
+ raise typer.BadParameter(
260
+ "Provide at least one of: --name, --visibility, --color, --icon, "
261
+ "--display-pipefy-header / --no-display-pipefy-header."
262
+ )
263
+
264
+ _reject_blank_optional_string(name, "--name")
265
+ _reject_blank_optional_string(color, "--color")
266
+ _reject_blank_optional_string(icon, "--icon")
267
+
268
+ async def factory(client: PipefyClient):
269
+ update_kwargs: dict[str, str | bool] = {}
270
+ if name is not None:
271
+ update_kwargs["name"] = name.strip()
272
+ if visibility is not None:
273
+ update_kwargs["visibility"] = visibility
274
+ if color is not None:
275
+ update_kwargs["color"] = color.strip()
276
+ if icon is not None:
277
+ update_kwargs["icon"] = icon.strip()
278
+ if display_pipefy_header is not None:
279
+ update_kwargs["display_pipefy_header"] = display_pipefy_header
280
+ return await client.update_portal(portal_uuid, **update_kwargs)
281
+
282
+ run_cli_command(ctx, json_out, factory)
283
+
284
+
285
+ @portal_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
286
+ def portal_delete(
287
+ ctx: typer.Context,
288
+ portal_uuid: str = resource_id_argument(help="Portal UUID."),
289
+ yes: bool = typer.Option(
290
+ False,
291
+ "--yes",
292
+ "-y",
293
+ help="Skip interactive confirmation.",
294
+ ),
295
+ json_out: bool = typer.Option(
296
+ False,
297
+ "--json",
298
+ "-j",
299
+ help="Print machine-readable JSON to stdout.",
300
+ ),
301
+ ) -> None:
302
+ """Delete a portal permanently."""
303
+
304
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
305
+ confirm_destructive(yes=yes, description=f"portal {portal_uuid}")
306
+
307
+ async def factory(client: PipefyClient):
308
+ return await client.delete_portal(portal_uuid)
309
+
310
+ run_cli_command(ctx, json_out, factory)
311
+
312
+
313
+ @page_app.command("create")
314
+ def portal_page_create(
315
+ ctx: typer.Context,
316
+ portal_uuid: str = typer.Option(
317
+ ...,
318
+ "--portal-uuid",
319
+ help="Parent portal interface UUID.",
320
+ ),
321
+ title: str = typer.Option(..., "--title", help="Page title."),
322
+ description: str | None = typer.Option(
323
+ None,
324
+ "--description",
325
+ help="Optional page description.",
326
+ ),
327
+ index: int | None = typer.Option(
328
+ None,
329
+ "--index",
330
+ min=0,
331
+ help="Optional sort index (non-negative).",
332
+ ),
333
+ json_out: bool = typer.Option(
334
+ False,
335
+ "--json",
336
+ "-j",
337
+ help="Print machine-readable JSON to stdout.",
338
+ ),
339
+ ) -> None:
340
+ """Create a portal page (bootstrap template when elements omitted on the API)."""
341
+
342
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
343
+ _reject_blank_optional_string(title, "--title")
344
+ _reject_blank_optional_string(description, "--description")
345
+
346
+ async def factory(client: PipefyClient):
347
+ return await client.create_portal_page(
348
+ portal_uuid,
349
+ title.strip(),
350
+ description=description.strip() if description is not None else None,
351
+ index=index,
352
+ )
353
+
354
+ run_cli_command(ctx, json_out, factory)
355
+
356
+
357
+ @page_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
358
+ def portal_page_update(
359
+ ctx: typer.Context,
360
+ portal_uuid: str = resource_id_argument(help="Parent portal interface UUID."),
361
+ page_uuid: str = resource_id_argument(help="Page UUID."),
362
+ title: str | None = typer.Option(None, "--title", help="New page title."),
363
+ description: str | None = typer.Option(
364
+ None,
365
+ "--description",
366
+ help="New page description.",
367
+ ),
368
+ index: int | None = typer.Option(
369
+ None,
370
+ "--index",
371
+ min=0,
372
+ help="New sort index (non-negative).",
373
+ ),
374
+ json_out: bool = typer.Option(
375
+ False,
376
+ "--json",
377
+ "-j",
378
+ help="Print machine-readable JSON to stdout.",
379
+ ),
380
+ ) -> None:
381
+ """Update portal page metadata (pass at least one attribute)."""
382
+
383
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
384
+ page_uuid = _require_non_empty_portal_uuid(page_uuid)
385
+
386
+ if all(x is None for x in (title, description, index)):
387
+ raise typer.BadParameter(
388
+ "Provide at least one of: --title, --description, --index."
389
+ )
390
+
391
+ _reject_blank_optional_string(title, "--title")
392
+ _reject_blank_optional_string(description, "--description")
393
+
394
+ async def factory(client: PipefyClient):
395
+ update_kwargs: dict[str, str | int] = {}
396
+ if title is not None:
397
+ update_kwargs["title"] = title.strip()
398
+ if description is not None:
399
+ update_kwargs["description"] = description.strip()
400
+ if index is not None:
401
+ update_kwargs["index"] = index
402
+ return await client.update_portal_page(
403
+ portal_uuid,
404
+ page_uuid,
405
+ **update_kwargs,
406
+ )
407
+
408
+ run_cli_command(ctx, json_out, factory)
409
+
410
+
411
+ @page_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
412
+ def portal_page_delete(
413
+ ctx: typer.Context,
414
+ portal_uuid: str = resource_id_argument(help="Parent portal interface UUID."),
415
+ page_uuid: str = resource_id_argument(help="Page UUID."),
416
+ yes: bool = typer.Option(
417
+ False,
418
+ "--yes",
419
+ "-y",
420
+ help="Skip interactive confirmation.",
421
+ ),
422
+ json_out: bool = typer.Option(
423
+ False,
424
+ "--json",
425
+ "-j",
426
+ help="Print machine-readable JSON to stdout.",
427
+ ),
428
+ ) -> None:
429
+ """Delete a portal page permanently."""
430
+
431
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
432
+ page_uuid = _require_non_empty_portal_uuid(page_uuid)
433
+ confirm_destructive(
434
+ yes=yes,
435
+ description=f"page {page_uuid} on portal {portal_uuid}",
436
+ )
437
+
438
+ async def factory(client: PipefyClient):
439
+ return await client.delete_portal_page(portal_uuid, page_uuid)
440
+
441
+ run_cli_command(ctx, json_out, factory)
442
+
443
+
444
+ @page_app.command("sort")
445
+ def portal_page_sort(
446
+ ctx: typer.Context,
447
+ portal_uuid: str = typer.Option(
448
+ ...,
449
+ "--portal-uuid",
450
+ help="Parent portal interface UUID.",
451
+ ),
452
+ page_ids: str | None = typer.Option(
453
+ None,
454
+ "--page-ids",
455
+ help="Comma-separated ordered page UUIDs.",
456
+ ),
457
+ ids_json: str | None = typer.Option(
458
+ None,
459
+ "--ids-json",
460
+ help="JSON array of ordered page UUIDs.",
461
+ ),
462
+ json_out: bool = typer.Option(
463
+ False,
464
+ "--json",
465
+ "-j",
466
+ help="Print machine-readable JSON to stdout.",
467
+ ),
468
+ ) -> None:
469
+ """Reorder portal pages."""
470
+
471
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
472
+ ordered_page_ids = _parse_page_ids_for_sort(page_ids, ids_json)
473
+
474
+ async def factory(client: PipefyClient):
475
+ return await client.sort_portal_pages(portal_uuid, ordered_page_ids)
476
+
477
+ run_cli_command(ctx, json_out, factory)
478
+
479
+
480
+ @layout_app.command("update")
481
+ def portal_page_layout_update(
482
+ ctx: typer.Context,
483
+ page_id: str = typer.Option(..., "--page-id", help="Page UUID."),
484
+ layout: str = typer.Option(
485
+ ...,
486
+ "--layout",
487
+ help="Layout JSON for updatePageLayout.",
488
+ ),
489
+ json_out: bool = typer.Option(
490
+ False,
491
+ "--json",
492
+ "-j",
493
+ help="Print machine-readable JSON to stdout.",
494
+ ),
495
+ ) -> None:
496
+ """Update a portal page grid layout."""
497
+
498
+ page_id = _require_non_empty_portal_uuid(page_id)
499
+ layout_obj = parse_json_object(layout, "--layout")
500
+ if layout_obj is None:
501
+ raise typer.BadParameter("--layout must be a JSON object.")
502
+
503
+ async def factory(client: PipefyClient):
504
+ return await client.update_portal_page_layout(page_id, layout_obj)
505
+
506
+ run_cli_command(ctx, json_out, factory)
507
+
508
+
509
+ @element_app.command("create")
510
+ def portal_element_create(
511
+ ctx: typer.Context,
512
+ page_id: str = typer.Option(..., "--page-id", help="Parent page UUID."),
513
+ type: str = typer.Option(
514
+ ...,
515
+ "--type",
516
+ help="InterfacePageElementType value (e.g. forms, link).",
517
+ ),
518
+ metadata: str = typer.Option(
519
+ None,
520
+ "--metadata",
521
+ help="Element metadata JSON (required; full shape depends on --type).",
522
+ ),
523
+ data_sources: str | None = typer.Option(
524
+ None,
525
+ "--data-sources",
526
+ help="Optional JSON array of data source bindings (forms elements).",
527
+ ),
528
+ json_out: bool = typer.Option(
529
+ False,
530
+ "--json",
531
+ "-j",
532
+ help="Print machine-readable JSON to stdout.",
533
+ ),
534
+ ) -> None:
535
+ """Create a portal page element."""
536
+
537
+ page_id = _require_non_empty_portal_uuid(page_id)
538
+ metadata_obj = _parse_required_metadata_json(metadata, "--metadata")
539
+ data_sources_list = _parse_optional_data_sources_json(data_sources)
540
+
541
+ try:
542
+ validated = CreatePortalElementInput.model_validate(
543
+ {
544
+ "page_id": page_id,
545
+ "type": type,
546
+ "metadata": metadata_obj,
547
+ "data_sources": data_sources_list or [],
548
+ }
549
+ )
550
+ except ValidationError as exc:
551
+ raise typer.BadParameter(str(exc)) from exc
552
+
553
+ create_kwargs = _portal_element_create_kwargs(validated)
554
+
555
+ async def factory(client: PipefyClient):
556
+ return await client.create_portal_element(validated.page_id, **create_kwargs)
557
+
558
+ run_cli_command(ctx, json_out, factory)
559
+
560
+
561
+ @element_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
562
+ def portal_element_update(
563
+ ctx: typer.Context,
564
+ element_id: str = resource_id_argument(help="Element UUID."),
565
+ page_id: str = resource_id_argument(help="Parent page UUID."),
566
+ type: str = typer.Option(
567
+ ...,
568
+ "--type",
569
+ help="Element type for metadata validation (full metadata replace on API).",
570
+ ),
571
+ metadata: str = typer.Option(
572
+ None,
573
+ "--metadata",
574
+ help="Complete element metadata JSON (Pipefy replaces the whole blob).",
575
+ ),
576
+ data_sources: str | None = typer.Option(
577
+ None,
578
+ "--data-sources",
579
+ help="Optional JSON array of data source bindings.",
580
+ ),
581
+ json_out: bool = typer.Option(
582
+ False,
583
+ "--json",
584
+ "-j",
585
+ help="Print machine-readable JSON to stdout.",
586
+ ),
587
+ ) -> None:
588
+ """Update a portal page element (send the full metadata blob)."""
589
+
590
+ element_id = _require_non_empty_portal_uuid(element_id)
591
+ page_id = _require_non_empty_portal_uuid(page_id)
592
+ metadata_obj = _parse_required_metadata_json(metadata, "--metadata")
593
+ data_sources_list = _parse_optional_data_sources_json(data_sources)
594
+
595
+ try:
596
+ validated = UpdatePortalElementInput.model_validate(
597
+ {
598
+ "element_id": element_id,
599
+ "page_id": page_id,
600
+ "type": type,
601
+ "metadata": metadata_obj,
602
+ "data_sources": data_sources_list or [],
603
+ }
604
+ )
605
+ except ValidationError as exc:
606
+ raise typer.BadParameter(str(exc)) from exc
607
+
608
+ update_kwargs = _portal_element_update_kwargs(validated)
609
+
610
+ async def factory(client: PipefyClient):
611
+ return await client.update_portal_element(
612
+ validated.element_id,
613
+ validated.page_id,
614
+ **update_kwargs,
615
+ )
616
+
617
+ run_cli_command(ctx, json_out, factory)
618
+
619
+
620
+ @element_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
621
+ def portal_element_delete(
622
+ ctx: typer.Context,
623
+ element_id: str = resource_id_argument(help="Element UUID."),
624
+ page_id: str = resource_id_argument(help="Parent page UUID."),
625
+ yes: bool = typer.Option(
626
+ False,
627
+ "--yes",
628
+ "-y",
629
+ help="Skip interactive confirmation.",
630
+ ),
631
+ json_out: bool = typer.Option(
632
+ False,
633
+ "--json",
634
+ "-j",
635
+ help="Print machine-readable JSON to stdout.",
636
+ ),
637
+ ) -> None:
638
+ """Delete a portal page element permanently."""
639
+
640
+ element_id = _require_non_empty_portal_uuid(element_id)
641
+ page_id = _require_non_empty_portal_uuid(page_id)
642
+ confirm_destructive(
643
+ yes=yes,
644
+ description=f"element {element_id} on page {page_id}",
645
+ )
646
+
647
+ async def factory(client: PipefyClient):
648
+ return await client.delete_portal_element(element_id, page_id)
649
+
650
+ run_cli_command(ctx, json_out, factory)
651
+
652
+
653
+ @element_app.command("duplicate")
654
+ def portal_element_duplicate(
655
+ ctx: typer.Context,
656
+ element_id: str = typer.Option(
657
+ ...,
658
+ "--element-id",
659
+ help="Element UUID to duplicate.",
660
+ ),
661
+ portal_uuid: str = typer.Option(
662
+ ...,
663
+ "--portal-uuid",
664
+ help="Portal interface UUID that owns the page.",
665
+ ),
666
+ page_id: str = typer.Option(
667
+ ...,
668
+ "--page-id",
669
+ help="Page UUID that contains the element.",
670
+ ),
671
+ json_out: bool = typer.Option(
672
+ False,
673
+ "--json",
674
+ "-j",
675
+ help="Print machine-readable JSON to stdout.",
676
+ ),
677
+ ) -> None:
678
+ """Duplicate a portal page element on the same page (source portal + page UUIDs)."""
679
+
680
+ element_id = _require_non_empty_portal_uuid(element_id)
681
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
682
+ page_id = _require_non_empty_portal_uuid(page_id)
683
+
684
+ async def factory(client: PipefyClient):
685
+ return await client.duplicate_portal_element(
686
+ element_id=element_id,
687
+ portal_uuid=portal_uuid,
688
+ page_id=page_id,
689
+ )
690
+
691
+ run_cli_command(ctx, json_out, factory)
692
+
693
+
694
+ @sub_portal_app.command("create")
695
+ def portal_sub_portal_create(
696
+ ctx: typer.Context,
697
+ main_portal_uuid: str = typer.Option(
698
+ ...,
699
+ "--main-portal-uuid",
700
+ help="Parent main portal interface UUID.",
701
+ ),
702
+ name: str | None = typer.Option(None, "--name", help="Optional sub-portal name."),
703
+ json_out: bool = typer.Option(
704
+ False,
705
+ "--json",
706
+ "-j",
707
+ help="Print machine-readable JSON to stdout.",
708
+ ),
709
+ ) -> None:
710
+ """Create a sub-portal on a main portal."""
711
+
712
+ main_portal_uuid = _require_non_empty_portal_uuid(main_portal_uuid)
713
+ _reject_blank_optional_string(name, "--name")
714
+
715
+ async def factory(client: PipefyClient):
716
+ return await client.create_sub_portal(
717
+ main_portal_uuid,
718
+ name=name.strip() if name is not None else None,
719
+ )
720
+
721
+ run_cli_command(ctx, json_out, factory)
722
+
723
+
724
+ @sub_portal_app.command("attach", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
725
+ def portal_sub_portal_attach(
726
+ ctx: typer.Context,
727
+ portal_uuid: str = resource_id_argument(help="Main portal interface UUID."),
728
+ element_id: str = resource_id_argument(help="Page element UUID."),
729
+ sub_portal_uuid: str = resource_id_argument(help="Sub-portal UUID."),
730
+ json_out: bool = typer.Option(
731
+ False,
732
+ "--json",
733
+ "-j",
734
+ help="Print machine-readable JSON to stdout.",
735
+ ),
736
+ ) -> None:
737
+ """Attach a sub-portal to a portal page element."""
738
+
739
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
740
+ element_id = _require_non_empty_portal_uuid(element_id)
741
+ sub_portal_uuid = _require_non_empty_portal_uuid(sub_portal_uuid)
742
+
743
+ async def factory(client: PipefyClient):
744
+ return await client.update_sub_portal_element(
745
+ portal_uuid,
746
+ element_id,
747
+ sub_portal_uuid,
748
+ )
749
+
750
+ run_cli_command(ctx, json_out, factory)
751
+
752
+
753
+ @sub_portal_app.command("detach", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
754
+ def portal_sub_portal_detach(
755
+ ctx: typer.Context,
756
+ portal_uuid: str = resource_id_argument(help="Main portal interface UUID."),
757
+ element_id: str = resource_id_argument(help="Page element UUID."),
758
+ yes: bool = typer.Option(
759
+ False,
760
+ "--yes",
761
+ "-y",
762
+ help="Skip interactive confirmation.",
763
+ ),
764
+ json_out: bool = typer.Option(
765
+ False,
766
+ "--json",
767
+ "-j",
768
+ help="Print machine-readable JSON to stdout.",
769
+ ),
770
+ ) -> None:
771
+ """Detach a sub-portal from a portal page element."""
772
+
773
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
774
+ element_id = _require_non_empty_portal_uuid(element_id)
775
+ confirm_destructive(
776
+ yes=yes,
777
+ description=(
778
+ f"sub-portal wiring on element {element_id} on portal {portal_uuid}"
779
+ ),
780
+ )
781
+
782
+ async def factory(client: PipefyClient):
783
+ return await client.delete_sub_portal_element(portal_uuid, element_id)
784
+
785
+ run_cli_command(ctx, json_out, factory)
786
+
787
+
788
+ @sub_portal_app.command("publish", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
789
+ def portal_sub_portal_publish(
790
+ ctx: typer.Context,
791
+ portal_uuid: str = resource_id_argument(help="Main portal interface UUID."),
792
+ element_id: str = resource_id_argument(help="Page element UUID."),
793
+ sub_portal_uuid: str = resource_id_argument(help="Sub-portal UUID."),
794
+ json_out: bool = typer.Option(
795
+ False,
796
+ "--json",
797
+ "-j",
798
+ help="Print machine-readable JSON to stdout.",
799
+ ),
800
+ ) -> None:
801
+ """Publish a sub-portal on a portal page element."""
802
+
803
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
804
+ element_id = _require_non_empty_portal_uuid(element_id)
805
+ sub_portal_uuid = _require_non_empty_portal_uuid(sub_portal_uuid)
806
+
807
+ async def factory(client: PipefyClient):
808
+ return await client.publish_sub_portal(
809
+ portal_uuid,
810
+ element_id,
811
+ sub_portal_uuid,
812
+ )
813
+
814
+ run_cli_command(ctx, json_out, factory)
815
+
816
+
817
+ @sub_portal_app.command("unpublish", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
818
+ def portal_sub_portal_unpublish(
819
+ ctx: typer.Context,
820
+ portal_uuid: str = resource_id_argument(help="Main portal interface UUID."),
821
+ element_id: str = resource_id_argument(help="Page element UUID."),
822
+ json_out: bool = typer.Option(
823
+ False,
824
+ "--json",
825
+ "-j",
826
+ help="Print machine-readable JSON to stdout.",
827
+ ),
828
+ ) -> None:
829
+ """Unpublish a sub-portal from a portal page element."""
830
+
831
+ portal_uuid = _require_non_empty_portal_uuid(portal_uuid)
832
+ element_id = _require_non_empty_portal_uuid(element_id)
833
+
834
+ async def factory(client: PipefyClient):
835
+ return await client.unpublish_sub_portal(portal_uuid, element_id)
836
+
837
+ run_cli_command(ctx, json_out, factory)
838
+
839
+
840
+ @sub_portal_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
841
+ def portal_sub_portal_delete(
842
+ ctx: typer.Context,
843
+ sub_portal_uuid: str = resource_id_argument(help="Sub-portal UUID."),
844
+ yes: bool = typer.Option(
845
+ False,
846
+ "--yes",
847
+ "-y",
848
+ help="Skip interactive confirmation.",
849
+ ),
850
+ json_out: bool = typer.Option(
851
+ False,
852
+ "--json",
853
+ "-j",
854
+ help="Print machine-readable JSON to stdout.",
855
+ ),
856
+ ) -> None:
857
+ """Delete a sub-portal permanently."""
858
+
859
+ sub_portal_uuid = _require_non_empty_portal_uuid(sub_portal_uuid)
860
+ confirm_destructive(yes=yes, description=f"sub-portal {sub_portal_uuid}")
861
+
862
+ async def factory(client: PipefyClient):
863
+ return await client.delete_sub_portal(sub_portal_uuid)
864
+
865
+ run_cli_command(ctx, json_out, factory)
866
+
867
+
868
+ page_app.add_typer(layout_app, name="layout")
869
+ portal_app.add_typer(page_app, name="page")
870
+ portal_app.add_typer(element_app, name="element")
871
+ portal_app.add_typer(sub_portal_app, name="sub-portal")