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,126 @@
|
|
|
1
|
+
"""Pipe member 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
|
+
confirm_destructive,
|
|
13
|
+
run_cli_command,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
member_app = typer.Typer(help="Pipe member operations.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_members_json(raw: str) -> list[dict[str, Any]]:
|
|
20
|
+
try:
|
|
21
|
+
parsed = json.loads(raw)
|
|
22
|
+
except json.JSONDecodeError as exc:
|
|
23
|
+
raise typer.BadParameter(f"Invalid JSON for --members: {exc}") from exc
|
|
24
|
+
if not isinstance(parsed, list) or not parsed:
|
|
25
|
+
raise typer.BadParameter("--members must be a non-empty JSON array of objects.")
|
|
26
|
+
for i, item in enumerate(parsed):
|
|
27
|
+
if not isinstance(item, dict):
|
|
28
|
+
raise typer.BadParameter(f"--members[{i}] must be an object.")
|
|
29
|
+
if "email" not in item or "role_name" not in item:
|
|
30
|
+
raise typer.BadParameter(
|
|
31
|
+
f"--members[{i}] must include 'email' and 'role_name'."
|
|
32
|
+
)
|
|
33
|
+
return parsed
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_user_ids(raw: str) -> list[str]:
|
|
37
|
+
parts = [p.strip() for p in raw.replace(",", " ").split() if p.strip()]
|
|
38
|
+
if not parts:
|
|
39
|
+
raise typer.BadParameter(
|
|
40
|
+
"Provide at least one user id (comma-separated or space-separated)."
|
|
41
|
+
)
|
|
42
|
+
return parts
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@member_app.command("list")
|
|
46
|
+
def member_list(
|
|
47
|
+
ctx: typer.Context,
|
|
48
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
49
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""List members of a pipe."""
|
|
52
|
+
|
|
53
|
+
async def factory(client: PipefyClient):
|
|
54
|
+
return await client.get_pipe_members(pipe_id)
|
|
55
|
+
|
|
56
|
+
run_cli_command(ctx, json_out, factory)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@member_app.command("invite")
|
|
60
|
+
def member_invite(
|
|
61
|
+
ctx: typer.Context,
|
|
62
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
63
|
+
members_json: str = typer.Option(
|
|
64
|
+
...,
|
|
65
|
+
"--members",
|
|
66
|
+
help='JSON array: [{"email":"a@b.com","role_name":"admin"}, ...].',
|
|
67
|
+
),
|
|
68
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Invite users to a pipe."""
|
|
71
|
+
|
|
72
|
+
members = _parse_members_json(members_json)
|
|
73
|
+
|
|
74
|
+
async def factory(client: PipefyClient):
|
|
75
|
+
return await client.invite_members(pipe_id, members)
|
|
76
|
+
|
|
77
|
+
run_cli_command(ctx, json_out, factory)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@member_app.command("remove")
|
|
81
|
+
def member_remove(
|
|
82
|
+
ctx: typer.Context,
|
|
83
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
84
|
+
user_ids: str = typer.Option(
|
|
85
|
+
...,
|
|
86
|
+
"--user-ids",
|
|
87
|
+
help="Comma-separated Pipefy user ids or UUIDs to remove from the pipe.",
|
|
88
|
+
),
|
|
89
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
90
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Remove users from a pipe."""
|
|
93
|
+
|
|
94
|
+
ids = _parse_user_ids(user_ids)
|
|
95
|
+
|
|
96
|
+
confirm_destructive(
|
|
97
|
+
yes=yes, verb="remove", description=f"{len(ids)} member(s) from pipe {pipe_id}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def factory(client: PipefyClient):
|
|
101
|
+
return await client.remove_members_from_pipe(pipe_id, ids)
|
|
102
|
+
|
|
103
|
+
run_cli_command(ctx, json_out, factory)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@member_app.command("set-role")
|
|
107
|
+
def member_set_role(
|
|
108
|
+
ctx: typer.Context,
|
|
109
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
110
|
+
member_id: str = typer.Option(..., "--member", help="Member user id."),
|
|
111
|
+
role_name: str = typer.Option(
|
|
112
|
+
..., "--role", help="Role name (e.g. member, admin)."
|
|
113
|
+
),
|
|
114
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Set a member's role on a pipe."""
|
|
117
|
+
|
|
118
|
+
rn = role_name.strip()
|
|
119
|
+
if not rn:
|
|
120
|
+
typer.echo("--role must be non-empty.", err=True)
|
|
121
|
+
raise typer.Exit(2)
|
|
122
|
+
|
|
123
|
+
async def factory(client: PipefyClient):
|
|
124
|
+
return await client.set_role(pipe_id, member_id, rn)
|
|
125
|
+
|
|
126
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Organization metadata (read-only)."""
|
|
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
|
+
run_cli_command,
|
|
11
|
+
settings_and_token,
|
|
12
|
+
validate_positional_id,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
org_app = typer.Typer(help="Organization operations.", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
_MISSING_ORG_ID = (
|
|
18
|
+
"Missing organization id. Pass ORGANIZATION_ID as the first argument, set "
|
|
19
|
+
"PIPEFY_ORG_ID in the environment, or obtain an id from "
|
|
20
|
+
"`pipefy pipe list --json` (organizations[].id)."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _optional_org_id(value: str | None) -> str | None:
|
|
25
|
+
if value is None:
|
|
26
|
+
return None
|
|
27
|
+
return validate_positional_id(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@org_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
31
|
+
def org_get(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
organization_id: str | None = typer.Argument(
|
|
34
|
+
None,
|
|
35
|
+
help=(
|
|
36
|
+
"Numeric organization id. Omit when PIPEFY_ORG_ID is set "
|
|
37
|
+
"(see docs/config.md)."
|
|
38
|
+
),
|
|
39
|
+
callback=_optional_org_id,
|
|
40
|
+
),
|
|
41
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Fetch organization details (``get_organization``)."""
|
|
44
|
+
|
|
45
|
+
pipefy_settings, _token = settings_and_token(ctx)
|
|
46
|
+
resolved = (organization_id or "").strip()
|
|
47
|
+
if not resolved:
|
|
48
|
+
env_default = pipefy_settings.org_id
|
|
49
|
+
resolved = env_default.strip() if env_default else ""
|
|
50
|
+
|
|
51
|
+
if not resolved:
|
|
52
|
+
typer.echo(_MISSING_ORG_ID, err=True)
|
|
53
|
+
raise typer.Exit(2)
|
|
54
|
+
|
|
55
|
+
oid = resolved
|
|
56
|
+
|
|
57
|
+
async def factory(client: PipefyClient):
|
|
58
|
+
return await client.get_organization(oid)
|
|
59
|
+
|
|
60
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Phase subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import PipefyClient
|
|
9
|
+
from pipefy_sdk.phase_inventory import (
|
|
10
|
+
get_phase_not_found_message,
|
|
11
|
+
is_get_phase_not_found_error,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from pipefy_cli.commands._common import (
|
|
15
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
16
|
+
confirm_destructive,
|
|
17
|
+
parse_json_object,
|
|
18
|
+
resource_id_argument,
|
|
19
|
+
run_cli_command,
|
|
20
|
+
validate_cards_page_size,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
phase_app = typer.Typer(help="Pipe phase operations.", no_args_is_help=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@phase_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
27
|
+
def phase_get(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
phase_id: str = resource_id_argument(help="Phase id."),
|
|
30
|
+
json_out: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--json",
|
|
33
|
+
"-j",
|
|
34
|
+
help="Print machine-readable JSON to stdout.",
|
|
35
|
+
),
|
|
36
|
+
required_only: bool = typer.Option(
|
|
37
|
+
False,
|
|
38
|
+
"--required-only",
|
|
39
|
+
help="Return only required fields (same query as MCP get_phase_fields).",
|
|
40
|
+
),
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Load phase metadata and fields by id (via ``get_phase_fields``)."""
|
|
43
|
+
|
|
44
|
+
async def factory(client: PipefyClient):
|
|
45
|
+
return await client.get_phase_fields(phase_id, required_only=required_only)
|
|
46
|
+
|
|
47
|
+
run_cli_command(ctx, json_out, factory)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@phase_app.command(
|
|
51
|
+
"targets",
|
|
52
|
+
context_settings=ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
53
|
+
)
|
|
54
|
+
def phase_targets(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
phase_id: str = resource_id_argument(
|
|
57
|
+
help="Source phase id (card's current phase)."
|
|
58
|
+
),
|
|
59
|
+
json_out: bool = typer.Option(
|
|
60
|
+
False,
|
|
61
|
+
"--json",
|
|
62
|
+
"-j",
|
|
63
|
+
help="Print machine-readable JSON to stdout.",
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""List phases a card may move to from this phase (UI transition rules)."""
|
|
67
|
+
|
|
68
|
+
async def factory(client: PipefyClient):
|
|
69
|
+
return await client.get_phase_allowed_move_targets(phase_id)
|
|
70
|
+
|
|
71
|
+
run_cli_command(ctx, json_out, factory)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@phase_app.command(
|
|
75
|
+
"count",
|
|
76
|
+
context_settings=ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
77
|
+
)
|
|
78
|
+
def phase_count(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
phase_id: str = resource_id_argument(help="Phase id."),
|
|
81
|
+
json_out: bool = typer.Option(
|
|
82
|
+
False,
|
|
83
|
+
"--json",
|
|
84
|
+
"-j",
|
|
85
|
+
help="Print machine-readable JSON to stdout.",
|
|
86
|
+
),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Return native ``Phase.cards_count`` for a phase (fast inventory).
|
|
89
|
+
|
|
90
|
+
On the start-form phase, ``cards_count`` may be 0 while cards still exist;
|
|
91
|
+
use ``pipefy phase cards`` to list cards when the count looks wrong.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
async def factory(client: PipefyClient):
|
|
95
|
+
try:
|
|
96
|
+
return await client.get_phase(phase_id)
|
|
97
|
+
except ValueError as exc:
|
|
98
|
+
if is_get_phase_not_found_error(exc):
|
|
99
|
+
raise ValueError(get_phase_not_found_message(phase_id)) from exc
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
run_cli_command(ctx, json_out, factory)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@phase_app.command("cards", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
106
|
+
def phase_cards(
|
|
107
|
+
ctx: typer.Context,
|
|
108
|
+
phase_id: str = resource_id_argument(help="Phase id."),
|
|
109
|
+
first: int = typer.Option(
|
|
110
|
+
50,
|
|
111
|
+
"--first",
|
|
112
|
+
help="Max cards per page (1-500).",
|
|
113
|
+
),
|
|
114
|
+
after: str | None = typer.Option(
|
|
115
|
+
None,
|
|
116
|
+
"--after",
|
|
117
|
+
help="Cursor from pageInfo.endCursor of a previous call.",
|
|
118
|
+
),
|
|
119
|
+
include_fields: bool = typer.Option(
|
|
120
|
+
False,
|
|
121
|
+
"--include-fields",
|
|
122
|
+
help="Include each card's custom fields in the response.",
|
|
123
|
+
),
|
|
124
|
+
json_out: bool = typer.Option(
|
|
125
|
+
False,
|
|
126
|
+
"--json",
|
|
127
|
+
"-j",
|
|
128
|
+
help="Print machine-readable JSON to stdout.",
|
|
129
|
+
),
|
|
130
|
+
) -> None:
|
|
131
|
+
"""List cards in a phase (``Phase.cards`` pagination)."""
|
|
132
|
+
|
|
133
|
+
first = validate_cards_page_size(first)
|
|
134
|
+
|
|
135
|
+
async def factory(client: PipefyClient):
|
|
136
|
+
return await client.get_phase_cards(
|
|
137
|
+
phase_id,
|
|
138
|
+
first=first,
|
|
139
|
+
after=after,
|
|
140
|
+
include_fields=include_fields,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
run_cli_command(ctx, json_out, factory)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@phase_app.command("create")
|
|
147
|
+
def phase_create(
|
|
148
|
+
ctx: typer.Context,
|
|
149
|
+
pipe_id: str = typer.Option(
|
|
150
|
+
..., "--pipe", help="Pipe id that will contain the phase."
|
|
151
|
+
),
|
|
152
|
+
name: str = typer.Option(..., "--name", "-n", help="Phase display name."),
|
|
153
|
+
done: bool = typer.Option(False, "--done", help="Mark as a final/done phase."),
|
|
154
|
+
index: float | None = typer.Option(
|
|
155
|
+
None,
|
|
156
|
+
"--index",
|
|
157
|
+
help="Optional position index within the pipe.",
|
|
158
|
+
),
|
|
159
|
+
description: str | None = typer.Option(None, "--description", "-d"),
|
|
160
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Create a phase in a pipe."""
|
|
163
|
+
|
|
164
|
+
stripped = name.strip()
|
|
165
|
+
if not stripped:
|
|
166
|
+
typer.echo("Phase name must be non-empty.", err=True)
|
|
167
|
+
raise typer.Exit(2)
|
|
168
|
+
|
|
169
|
+
async def factory(client: PipefyClient):
|
|
170
|
+
return await client.create_phase(
|
|
171
|
+
pipe_id,
|
|
172
|
+
stripped,
|
|
173
|
+
done=done,
|
|
174
|
+
index=index,
|
|
175
|
+
description=description,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
run_cli_command(ctx, json_out, factory)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@phase_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
182
|
+
def phase_update(
|
|
183
|
+
ctx: typer.Context,
|
|
184
|
+
phase_id: str = resource_id_argument(help="Phase id."),
|
|
185
|
+
name: str | None = typer.Option(None, "--name", "-n"),
|
|
186
|
+
description: str | None = typer.Option(None, "--description", "-d"),
|
|
187
|
+
done: bool | None = typer.Option(
|
|
188
|
+
None,
|
|
189
|
+
"--done/--no-done",
|
|
190
|
+
help="Set or clear the final-phase flag.",
|
|
191
|
+
),
|
|
192
|
+
color: str | None = typer.Option(None, "--color"),
|
|
193
|
+
lateness_time: int | None = typer.Option(None, "--lateness-time"),
|
|
194
|
+
can_receive_from_draft: bool | None = typer.Option(
|
|
195
|
+
None,
|
|
196
|
+
"--can-receive-from-draft/--no-can-receive-from-draft",
|
|
197
|
+
),
|
|
198
|
+
extra_json: str | None = typer.Option(
|
|
199
|
+
None,
|
|
200
|
+
"--extra",
|
|
201
|
+
help="JSON object merged into UpdatePhaseInput (snake_case keys).",
|
|
202
|
+
),
|
|
203
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Update a phase (Pipefy ``UpdatePhaseInput``). Resolves current name when omitted."""
|
|
206
|
+
|
|
207
|
+
extra = parse_json_object(extra_json, "--extra")
|
|
208
|
+
update_attrs: dict[str, Any] = {}
|
|
209
|
+
if name is not None:
|
|
210
|
+
update_attrs["name"] = name
|
|
211
|
+
if description is not None:
|
|
212
|
+
update_attrs["description"] = description
|
|
213
|
+
if done is not None:
|
|
214
|
+
update_attrs["done"] = done
|
|
215
|
+
if color is not None:
|
|
216
|
+
update_attrs["color"] = color
|
|
217
|
+
if lateness_time is not None:
|
|
218
|
+
update_attrs["lateness_time"] = lateness_time
|
|
219
|
+
if can_receive_from_draft is not None:
|
|
220
|
+
update_attrs["can_receive_card_directly_from_draft"] = can_receive_from_draft
|
|
221
|
+
if extra:
|
|
222
|
+
update_attrs.update(extra)
|
|
223
|
+
if not update_attrs:
|
|
224
|
+
typer.echo(
|
|
225
|
+
"Provide at least one of: --name, --description, --done, --color, "
|
|
226
|
+
"--lateness-time, --can-receive-from-draft, --extra.",
|
|
227
|
+
err=True,
|
|
228
|
+
)
|
|
229
|
+
raise typer.Exit(2)
|
|
230
|
+
|
|
231
|
+
async def factory(client: PipefyClient):
|
|
232
|
+
if "name" not in update_attrs:
|
|
233
|
+
phase_info = await client.get_phase_fields(phase_id)
|
|
234
|
+
current = phase_info.get("phase_name")
|
|
235
|
+
if not current:
|
|
236
|
+
raise typer.BadParameter(
|
|
237
|
+
f"Phase {phase_id} not found or has no name; pass --name explicitly."
|
|
238
|
+
)
|
|
239
|
+
update_attrs["name"] = current
|
|
240
|
+
return await client.update_phase(phase_id, **update_attrs)
|
|
241
|
+
|
|
242
|
+
run_cli_command(ctx, json_out, factory)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@phase_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
246
|
+
def phase_delete(
|
|
247
|
+
ctx: typer.Context,
|
|
248
|
+
phase_id: str = resource_id_argument(help="Phase id."),
|
|
249
|
+
yes: bool = typer.Option(
|
|
250
|
+
False,
|
|
251
|
+
"--yes",
|
|
252
|
+
"-y",
|
|
253
|
+
help="Skip confirmation prompt.",
|
|
254
|
+
),
|
|
255
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Delete a phase permanently."""
|
|
258
|
+
|
|
259
|
+
confirm_destructive(yes=yes, description=f"phase {phase_id}")
|
|
260
|
+
|
|
261
|
+
async def factory(client: PipefyClient):
|
|
262
|
+
return await client.delete_phase(phase_id)
|
|
263
|
+
|
|
264
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Pipe 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
|
+
pipe_app = typer.Typer(help="Pipe operations.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pipe_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
20
|
+
def pipe_get(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
pipe_id: str = resource_id_argument(help="Pipe id."),
|
|
23
|
+
json_out: bool = typer.Option(
|
|
24
|
+
False,
|
|
25
|
+
"--json",
|
|
26
|
+
"-j",
|
|
27
|
+
help="Print machine-readable JSON to stdout.",
|
|
28
|
+
),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Load a pipe by id (phases, labels, start form)."""
|
|
31
|
+
|
|
32
|
+
async def factory(client: PipefyClient):
|
|
33
|
+
return await client.get_pipe(pipe_id)
|
|
34
|
+
|
|
35
|
+
run_cli_command(ctx, json_out, factory)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pipe_app.command("list")
|
|
39
|
+
def pipe_list(
|
|
40
|
+
ctx: typer.Context,
|
|
41
|
+
name: str | None = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--name",
|
|
44
|
+
"-n",
|
|
45
|
+
help="Optional pipe name filter (fuzzy match server-side).",
|
|
46
|
+
),
|
|
47
|
+
max_per_org: int = typer.Option(
|
|
48
|
+
500,
|
|
49
|
+
"--max-per-org",
|
|
50
|
+
min=1,
|
|
51
|
+
max=500,
|
|
52
|
+
help="Maximum pipes returned per organization (1-500).",
|
|
53
|
+
),
|
|
54
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Search pipes across organizations (same as MCP ``search_pipes``)."""
|
|
57
|
+
|
|
58
|
+
async def factory(client: PipefyClient):
|
|
59
|
+
return await client.search_pipes(name, max_pipes_per_org=max_per_org)
|
|
60
|
+
|
|
61
|
+
run_cli_command(ctx, json_out, factory)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pipe_app.command("create")
|
|
65
|
+
def pipe_create(
|
|
66
|
+
ctx: typer.Context,
|
|
67
|
+
name: str = typer.Argument(..., help="Display name for the new pipe."),
|
|
68
|
+
org_id: str = typer.Option(
|
|
69
|
+
...,
|
|
70
|
+
"--org",
|
|
71
|
+
help="Organization id that will own the pipe.",
|
|
72
|
+
),
|
|
73
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Create an empty pipe in an organization."""
|
|
76
|
+
|
|
77
|
+
stripped = name.strip()
|
|
78
|
+
if not stripped:
|
|
79
|
+
typer.echo("Pipe name must be non-empty.", err=True)
|
|
80
|
+
raise typer.Exit(2)
|
|
81
|
+
|
|
82
|
+
async def factory(client: PipefyClient):
|
|
83
|
+
return await client.create_pipe(stripped, org_id)
|
|
84
|
+
|
|
85
|
+
run_cli_command(ctx, json_out, factory)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pipe_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
89
|
+
def pipe_update(
|
|
90
|
+
ctx: typer.Context,
|
|
91
|
+
pipe_id: str = resource_id_argument(help="Pipe id."),
|
|
92
|
+
name: str | None = typer.Option(None, "--name"),
|
|
93
|
+
icon: str | None = typer.Option(None, "--icon"),
|
|
94
|
+
color: str | None = typer.Option(None, "--color"),
|
|
95
|
+
preferences_json: str | None = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"--preferences",
|
|
98
|
+
help="JSON object for pipe preferences (UpdatePipeInput).",
|
|
99
|
+
),
|
|
100
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Update pipe settings (pass at least one attribute)."""
|
|
103
|
+
|
|
104
|
+
preferences = parse_json_object(preferences_json, "--preferences")
|
|
105
|
+
if preferences == {}:
|
|
106
|
+
preferences = None
|
|
107
|
+
if all(x is None for x in (name, icon, color, preferences)):
|
|
108
|
+
raise typer.BadParameter(
|
|
109
|
+
"Provide at least one of: --name, --icon, --color, --preferences (non-empty)."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def factory(client: PipefyClient):
|
|
113
|
+
return await client.update_pipe(
|
|
114
|
+
pipe_id,
|
|
115
|
+
name=name,
|
|
116
|
+
icon=icon,
|
|
117
|
+
color=color,
|
|
118
|
+
preferences=preferences,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
run_cli_command(ctx, json_out, factory)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pipe_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
125
|
+
def pipe_delete(
|
|
126
|
+
ctx: typer.Context,
|
|
127
|
+
pipe_id: str = resource_id_argument(help="Pipe id."),
|
|
128
|
+
yes: bool = typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--yes",
|
|
131
|
+
"-y",
|
|
132
|
+
help="Skip confirmation prompt.",
|
|
133
|
+
),
|
|
134
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Delete a pipe permanently."""
|
|
137
|
+
|
|
138
|
+
confirm_destructive(yes=yes, description=f"pipe {pipe_id}")
|
|
139
|
+
|
|
140
|
+
async def factory(client: PipefyClient):
|
|
141
|
+
return await client.delete_pipe(pipe_id)
|
|
142
|
+
|
|
143
|
+
run_cli_command(ctx, json_out, factory)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pipe_app.command("clone", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
147
|
+
def pipe_clone(
|
|
148
|
+
ctx: typer.Context,
|
|
149
|
+
template_pipe_id: str = resource_id_argument(
|
|
150
|
+
help="Source pipe id to use as template.",
|
|
151
|
+
),
|
|
152
|
+
org_id: str | None = typer.Option(
|
|
153
|
+
None,
|
|
154
|
+
"--org",
|
|
155
|
+
help="Optional organization id for the cloned pipe.",
|
|
156
|
+
),
|
|
157
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Clone a pipe from a template pipe id."""
|
|
160
|
+
|
|
161
|
+
async def factory(client: PipefyClient):
|
|
162
|
+
return await client.clone_pipe(
|
|
163
|
+
template_pipe_id,
|
|
164
|
+
organization_id=org_id,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
run_cli_command(ctx, json_out, factory)
|