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,113 @@
|
|
|
1
|
+
"""Phase field 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
|
+
field_app = typer.Typer(help="Phase field operations.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@field_app.command("list")
|
|
20
|
+
def field_list(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
phase_id: str = typer.Option(..., "--phase", help="Phase id."),
|
|
23
|
+
required_only: bool = typer.Option(
|
|
24
|
+
False,
|
|
25
|
+
"--required-only",
|
|
26
|
+
help="Return only required fields.",
|
|
27
|
+
),
|
|
28
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""List fields on a phase (``get_phase_fields``)."""
|
|
31
|
+
|
|
32
|
+
async def factory(client: PipefyClient):
|
|
33
|
+
return await client.get_phase_fields(phase_id, required_only=required_only)
|
|
34
|
+
|
|
35
|
+
run_cli_command(ctx, json_out, factory)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@field_app.command("create")
|
|
39
|
+
def field_create(
|
|
40
|
+
ctx: typer.Context,
|
|
41
|
+
phase_id: str = typer.Option(..., "--phase", help="Phase id."),
|
|
42
|
+
label: str = typer.Option(..., "--label", "-l", help="Field label in the UI."),
|
|
43
|
+
field_type: str = typer.Option(
|
|
44
|
+
...,
|
|
45
|
+
"--type",
|
|
46
|
+
"-t",
|
|
47
|
+
help="Pipefy field type (e.g. short_text, number).",
|
|
48
|
+
),
|
|
49
|
+
extra_json: str | None = typer.Option(
|
|
50
|
+
None,
|
|
51
|
+
"--extra",
|
|
52
|
+
help="JSON object of extra CreatePhaseFieldInput fields.",
|
|
53
|
+
),
|
|
54
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Create a field on a phase."""
|
|
57
|
+
|
|
58
|
+
extra = parse_json_object(extra_json, "--extra") or {}
|
|
59
|
+
lab = label.strip()
|
|
60
|
+
ft = field_type.strip()
|
|
61
|
+
if not lab or not ft:
|
|
62
|
+
typer.echo("--label and --type must be non-empty.", err=True)
|
|
63
|
+
raise typer.Exit(2)
|
|
64
|
+
|
|
65
|
+
async def factory(client: PipefyClient):
|
|
66
|
+
return await client.create_phase_field(phase_id, lab, ft, **extra)
|
|
67
|
+
|
|
68
|
+
run_cli_command(ctx, json_out, factory)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@field_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
72
|
+
def field_update(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
field_id: str = resource_id_argument(help="Phase field id."),
|
|
75
|
+
extra_json: str | None = typer.Option(
|
|
76
|
+
None,
|
|
77
|
+
"--extra",
|
|
78
|
+
help="JSON object of UpdatePhaseFieldInput fields (snake_case / API keys).",
|
|
79
|
+
),
|
|
80
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Update a phase field (pass attributes via ``--extra`` JSON)."""
|
|
83
|
+
|
|
84
|
+
extra = parse_json_object(extra_json, "--extra")
|
|
85
|
+
if not extra:
|
|
86
|
+
raise typer.BadParameter("Provide --extra with a non-empty JSON object.")
|
|
87
|
+
|
|
88
|
+
async def factory(client: PipefyClient):
|
|
89
|
+
return await client.update_phase_field(field_id, **extra)
|
|
90
|
+
|
|
91
|
+
run_cli_command(ctx, json_out, factory)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@field_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
95
|
+
def field_delete(
|
|
96
|
+
ctx: typer.Context,
|
|
97
|
+
field_id: str = resource_id_argument(help="Phase field id."),
|
|
98
|
+
pipe_uuid: str | None = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"--pipe-uuid",
|
|
101
|
+
help="Optional pipe UUID (passed through to the API when required).",
|
|
102
|
+
),
|
|
103
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
104
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Delete a phase field permanently."""
|
|
107
|
+
|
|
108
|
+
confirm_destructive(yes=yes, description=f"phase field {field_id}")
|
|
109
|
+
|
|
110
|
+
async def factory(client: PipefyClient):
|
|
111
|
+
return await client.delete_phase_field(field_id, pipe_uuid=pipe_uuid)
|
|
112
|
+
|
|
113
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Field condition rules on phases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import PipefyClient
|
|
9
|
+
|
|
10
|
+
from pipefy_cli.commands._common import (
|
|
11
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
12
|
+
confirm_destructive,
|
|
13
|
+
parse_json_object,
|
|
14
|
+
parse_json_value,
|
|
15
|
+
resource_id_argument,
|
|
16
|
+
run_cli_command,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
field_condition_app = typer.Typer(
|
|
20
|
+
help="Field conditions (dynamic form rules on phases).",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@field_condition_app.command("list")
|
|
26
|
+
def field_condition_list(
|
|
27
|
+
ctx: typer.Context,
|
|
28
|
+
phase_id: str = typer.Option(
|
|
29
|
+
..., "--phase", help="Phase id that owns the conditions."
|
|
30
|
+
),
|
|
31
|
+
json_out: bool = typer.Option(
|
|
32
|
+
False,
|
|
33
|
+
"--json",
|
|
34
|
+
"-j",
|
|
35
|
+
help="Print machine-readable JSON to stdout.",
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""List field conditions on a phase (``get_field_conditions``)."""
|
|
39
|
+
|
|
40
|
+
async def factory(client: PipefyClient) -> dict[str, Any]:
|
|
41
|
+
raw = await client.get_field_conditions(phase_id)
|
|
42
|
+
phase = raw.get("phase")
|
|
43
|
+
if phase is None:
|
|
44
|
+
return {"success": False, "error": "Phase not found or access denied."}
|
|
45
|
+
rows = phase.get("fieldConditions") or []
|
|
46
|
+
return {
|
|
47
|
+
"success": True,
|
|
48
|
+
"message": "Field conditions loaded.",
|
|
49
|
+
"field_conditions": rows,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
run_cli_command(ctx, json_out, factory)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@field_condition_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
56
|
+
def field_condition_get(
|
|
57
|
+
ctx: typer.Context,
|
|
58
|
+
condition_id: str = resource_id_argument(help="Field condition id."),
|
|
59
|
+
json_out: bool = typer.Option(
|
|
60
|
+
False,
|
|
61
|
+
"--json",
|
|
62
|
+
"-j",
|
|
63
|
+
help="Print machine-readable JSON to stdout.",
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Load one field condition by id (``get_field_condition``)."""
|
|
67
|
+
|
|
68
|
+
async def factory(client: PipefyClient) -> dict[str, Any]:
|
|
69
|
+
raw = await client.get_field_condition(condition_id)
|
|
70
|
+
fc = raw.get("fieldCondition")
|
|
71
|
+
if fc is None:
|
|
72
|
+
return {
|
|
73
|
+
"success": False,
|
|
74
|
+
"error": "Field condition not found or access denied.",
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
"success": True,
|
|
78
|
+
"message": "Field condition loaded.",
|
|
79
|
+
"field_condition": fc,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
run_cli_command(ctx, json_out, factory)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@field_condition_app.command("create")
|
|
86
|
+
def field_condition_create(
|
|
87
|
+
ctx: typer.Context,
|
|
88
|
+
phase_id: str = typer.Option(..., "--phase", help="Phase id."),
|
|
89
|
+
name: str = typer.Option(..., "--name", "-n", help="Rule name (required by API)."),
|
|
90
|
+
condition: str = typer.Option(
|
|
91
|
+
...,
|
|
92
|
+
"--condition",
|
|
93
|
+
help=(
|
|
94
|
+
"JSON object: ConditionInput. Example: "
|
|
95
|
+
'\'{"expressions":[{"structure_id":0,"field_address":"<internal_id>",'
|
|
96
|
+
'"operation":"equals","value":"X"}],"expressions_structure":[[0]]}\'. '
|
|
97
|
+
"structure_id / expressions_structure entries are coerced to int by the SDK."
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
actions: str = typer.Option(
|
|
101
|
+
...,
|
|
102
|
+
"--actions",
|
|
103
|
+
help=(
|
|
104
|
+
"JSON array: action objects. Each item needs phaseFieldId (use the field's "
|
|
105
|
+
"internal_id from get_phase_fields) + actionId (hide|show). Example: "
|
|
106
|
+
'\'[{"phaseFieldId":"<internal_id>","actionId":"hide"}]\'.'
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
extra: str | None = typer.Option(
|
|
110
|
+
None,
|
|
111
|
+
"--extra",
|
|
112
|
+
help="Optional JSON object merged into create input (camelCase keys).",
|
|
113
|
+
),
|
|
114
|
+
json_out: bool = typer.Option(
|
|
115
|
+
False,
|
|
116
|
+
"--json",
|
|
117
|
+
"-j",
|
|
118
|
+
help="Print machine-readable JSON to stdout.",
|
|
119
|
+
),
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Create a field condition (``create_field_condition``)."""
|
|
122
|
+
cond_obj = parse_json_value(condition, "--condition")
|
|
123
|
+
if not isinstance(cond_obj, dict):
|
|
124
|
+
raise typer.BadParameter("--condition must be a JSON object")
|
|
125
|
+
act_parsed = parse_json_value(actions, "--actions")
|
|
126
|
+
if not isinstance(act_parsed, list):
|
|
127
|
+
raise typer.BadParameter("--actions must be a JSON array")
|
|
128
|
+
actions_list: list[dict[str, Any]] = []
|
|
129
|
+
for i, item in enumerate(act_parsed):
|
|
130
|
+
if not isinstance(item, dict):
|
|
131
|
+
raise typer.BadParameter(f"--actions[{i}] must be a JSON object")
|
|
132
|
+
actions_list.append(item)
|
|
133
|
+
extra_obj = parse_json_object(extra, "--extra") or {}
|
|
134
|
+
|
|
135
|
+
async def factory(client: PipefyClient):
|
|
136
|
+
return await client.create_field_condition(
|
|
137
|
+
phase_id,
|
|
138
|
+
cond_obj,
|
|
139
|
+
actions_list,
|
|
140
|
+
name=name,
|
|
141
|
+
**extra_obj,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
run_cli_command(ctx, json_out, factory)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@field_condition_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
148
|
+
def field_condition_update(
|
|
149
|
+
ctx: typer.Context,
|
|
150
|
+
condition_id: str = resource_id_argument(help="Field condition id."),
|
|
151
|
+
extra: str = typer.Option(
|
|
152
|
+
...,
|
|
153
|
+
"--extra",
|
|
154
|
+
help="JSON object: fields to patch (camelCase keys for UpdateFieldConditionInput).",
|
|
155
|
+
),
|
|
156
|
+
json_out: bool = typer.Option(
|
|
157
|
+
False,
|
|
158
|
+
"--json",
|
|
159
|
+
"-j",
|
|
160
|
+
help="Print machine-readable JSON to stdout.",
|
|
161
|
+
),
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Update a field condition (``update_field_condition``)."""
|
|
164
|
+
extra_obj = parse_json_value(extra, "--extra")
|
|
165
|
+
if not isinstance(extra_obj, dict):
|
|
166
|
+
raise typer.BadParameter("--extra must be a JSON object")
|
|
167
|
+
|
|
168
|
+
async def factory(client: PipefyClient):
|
|
169
|
+
return await client.update_field_condition(condition_id, **extra_obj)
|
|
170
|
+
|
|
171
|
+
run_cli_command(ctx, json_out, factory)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@field_condition_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
175
|
+
def field_condition_delete(
|
|
176
|
+
ctx: typer.Context,
|
|
177
|
+
condition_id: str = resource_id_argument(help="Field condition id."),
|
|
178
|
+
yes: bool = typer.Option(
|
|
179
|
+
False,
|
|
180
|
+
"--yes",
|
|
181
|
+
"-y",
|
|
182
|
+
help="Skip interactive confirmation.",
|
|
183
|
+
),
|
|
184
|
+
json_out: bool = typer.Option(
|
|
185
|
+
False,
|
|
186
|
+
"--json",
|
|
187
|
+
"-j",
|
|
188
|
+
help="Print machine-readable JSON to stdout.",
|
|
189
|
+
),
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Delete a field condition permanently (``delete_field_condition``)."""
|
|
192
|
+
confirm_destructive(
|
|
193
|
+
yes=yes,
|
|
194
|
+
description=f"field condition {condition_id}",
|
|
195
|
+
verb="delete",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def factory(client: PipefyClient):
|
|
199
|
+
return await client.delete_field_condition(condition_id)
|
|
200
|
+
|
|
201
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Raw GraphQL execution (escape hatch for agents)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from graphql import GraphQLSyntaxError, parse
|
|
11
|
+
from graphql.language.ast import OperationDefinitionNode, OperationType
|
|
12
|
+
from pipefy_sdk import PipefyClient
|
|
13
|
+
|
|
14
|
+
from pipefy_cli.commands._common import run_pipefy_client_coroutine
|
|
15
|
+
from pipefy_cli.output import render_json, render_rich
|
|
16
|
+
|
|
17
|
+
graphql_app = typer.Typer(
|
|
18
|
+
help="Execute arbitrary GraphQL (mutations require --yes). See docs/cli/self-healing.md.",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _document_contains_mutation(document: str) -> bool:
|
|
24
|
+
"""Return True when the document defines at least one mutation operation."""
|
|
25
|
+
try:
|
|
26
|
+
doc = parse(document)
|
|
27
|
+
except GraphQLSyntaxError:
|
|
28
|
+
return False
|
|
29
|
+
for defn in doc.definitions:
|
|
30
|
+
if (
|
|
31
|
+
isinstance(defn, OperationDefinitionNode)
|
|
32
|
+
and defn.operation == OperationType.MUTATION
|
|
33
|
+
):
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_vars_json(raw: str | None) -> dict[str, Any]:
|
|
39
|
+
"""Parse --vars JSON; default is empty object. Malformed JSON raises typer.Exit(2)."""
|
|
40
|
+
if raw is None or raw.strip() == "":
|
|
41
|
+
return {}
|
|
42
|
+
try:
|
|
43
|
+
parsed: Any = json.loads(raw)
|
|
44
|
+
except json.JSONDecodeError as exc:
|
|
45
|
+
typer.echo(f"Invalid JSON for --vars: {exc}", err=True)
|
|
46
|
+
raise typer.Exit(2) from exc
|
|
47
|
+
if not isinstance(parsed, dict):
|
|
48
|
+
typer.echo("--vars must be a JSON object.", err=True)
|
|
49
|
+
raise typer.Exit(2)
|
|
50
|
+
return parsed
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _run_graphql(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
factory: Callable[[PipefyClient], Awaitable[Any]],
|
|
56
|
+
) -> Any:
|
|
57
|
+
"""Run an async GraphQL call with shared auth."""
|
|
58
|
+
return run_pipefy_client_coroutine(ctx, factory)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@graphql_app.command("exec")
|
|
62
|
+
def graphql_exec(
|
|
63
|
+
ctx: typer.Context,
|
|
64
|
+
query: str = typer.Option(
|
|
65
|
+
...,
|
|
66
|
+
"--query",
|
|
67
|
+
"-q",
|
|
68
|
+
help="GraphQL document string (query or mutation).",
|
|
69
|
+
),
|
|
70
|
+
vars_json: str | None = typer.Option(
|
|
71
|
+
None,
|
|
72
|
+
"--vars",
|
|
73
|
+
help='Variables JSON object (default "{}" ).',
|
|
74
|
+
),
|
|
75
|
+
yes: bool = typer.Option(
|
|
76
|
+
False,
|
|
77
|
+
"--yes",
|
|
78
|
+
help="Acknowledge and run mutations (required when the document includes mutation).",
|
|
79
|
+
),
|
|
80
|
+
json_out: bool = typer.Option(
|
|
81
|
+
False,
|
|
82
|
+
"--json",
|
|
83
|
+
"-j",
|
|
84
|
+
help="Emit JSON to stdout (default is Rich for objects).",
|
|
85
|
+
),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Execute GraphQL via the SDK (``execute_graphql``).
|
|
88
|
+
|
|
89
|
+
Pass variables as JSON with ``--vars '{"key":"value"}'`` (not ``--variables``).
|
|
90
|
+
|
|
91
|
+
Mutations are rejected unless ``--yes`` is set (exit code 2). Pair with
|
|
92
|
+
``pipefy introspect`` to discover operation shapes (see docs/cli/self-healing.md).
|
|
93
|
+
"""
|
|
94
|
+
variables = _parse_vars_json(vars_json)
|
|
95
|
+
if _document_contains_mutation(query) and not yes:
|
|
96
|
+
typer.echo(
|
|
97
|
+
"This document includes a mutation; re-run with --yes to confirm, "
|
|
98
|
+
"or use read-only operations.",
|
|
99
|
+
err=True,
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(2)
|
|
102
|
+
|
|
103
|
+
async def factory(client: PipefyClient):
|
|
104
|
+
return await client.execute_graphql(query, variables)
|
|
105
|
+
|
|
106
|
+
payload = _run_graphql(ctx, factory)
|
|
107
|
+
if json_out:
|
|
108
|
+
render_json(payload)
|
|
109
|
+
else:
|
|
110
|
+
render_rich(payload)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""GraphQL schema introspection (types, root fields, search)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from pipefy_sdk import PipefyClient
|
|
7
|
+
|
|
8
|
+
from pipefy_cli.commands._common import run_pipefy_client_coroutine
|
|
9
|
+
from pipefy_cli.output import render_json, render_rich
|
|
10
|
+
|
|
11
|
+
introspect_app = typer.Typer(
|
|
12
|
+
help="Discover GraphQL types and operations (JSON default).",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
schema_app = typer.Typer(help="Schema-wide search.", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _emit_introspection(data: object, *, rich: bool) -> None:
|
|
19
|
+
"""Print introspection payload (JSON default, optional Rich)."""
|
|
20
|
+
if rich:
|
|
21
|
+
render_rich(data)
|
|
22
|
+
else:
|
|
23
|
+
render_json(data)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@introspect_app.command("type")
|
|
27
|
+
def introspect_type(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
name: str = typer.Argument(..., help="GraphQL type name (e.g. Card)."),
|
|
30
|
+
max_depth: int = typer.Option(
|
|
31
|
+
1, "--max-depth", help="Nested type resolution depth."
|
|
32
|
+
),
|
|
33
|
+
rich: bool = typer.Option(
|
|
34
|
+
False,
|
|
35
|
+
"--rich",
|
|
36
|
+
help="Pretty-print with Rich instead of JSON.",
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Introspect a schema type (``introspect_type``)."""
|
|
40
|
+
|
|
41
|
+
async def factory(client: PipefyClient):
|
|
42
|
+
return await client.introspect_type(name, max_depth=max_depth)
|
|
43
|
+
|
|
44
|
+
payload = run_pipefy_client_coroutine(ctx, factory)
|
|
45
|
+
_emit_introspection(payload, rich=rich)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@introspect_app.command("query")
|
|
49
|
+
def introspect_query(
|
|
50
|
+
ctx: typer.Context,
|
|
51
|
+
name: str = typer.Argument(..., help="Root query field name."),
|
|
52
|
+
max_depth: int = typer.Option(
|
|
53
|
+
1, "--max-depth", help="Nested type resolution depth."
|
|
54
|
+
),
|
|
55
|
+
rich: bool = typer.Option(
|
|
56
|
+
False, "--rich", help="Pretty-print with Rich instead of JSON."
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Introspect a root query field (``introspect_query``)."""
|
|
60
|
+
|
|
61
|
+
async def factory(client: PipefyClient):
|
|
62
|
+
return await client.introspect_query(name, max_depth=max_depth)
|
|
63
|
+
|
|
64
|
+
payload = run_pipefy_client_coroutine(ctx, factory)
|
|
65
|
+
_emit_introspection(payload, rich=rich)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@introspect_app.command("mutation")
|
|
69
|
+
def introspect_mutation(
|
|
70
|
+
ctx: typer.Context,
|
|
71
|
+
name: str = typer.Argument(..., help="Root mutation field name."),
|
|
72
|
+
max_depth: int = typer.Option(
|
|
73
|
+
1, "--max-depth", help="Nested type resolution depth."
|
|
74
|
+
),
|
|
75
|
+
rich: bool = typer.Option(
|
|
76
|
+
False, "--rich", help="Pretty-print with Rich instead of JSON."
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Introspect a root mutation field (``introspect_mutation``)."""
|
|
80
|
+
|
|
81
|
+
async def factory(client: PipefyClient):
|
|
82
|
+
return await client.introspect_mutation(name, max_depth=max_depth)
|
|
83
|
+
|
|
84
|
+
payload = run_pipefy_client_coroutine(ctx, factory)
|
|
85
|
+
_emit_introspection(payload, rich=rich)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@schema_app.command("search")
|
|
89
|
+
def introspect_schema_search(
|
|
90
|
+
ctx: typer.Context,
|
|
91
|
+
keyword: str = typer.Argument(..., help="Case-insensitive substring."),
|
|
92
|
+
kind: str | None = typer.Option(
|
|
93
|
+
None,
|
|
94
|
+
"--kind",
|
|
95
|
+
help="Optional GraphQL type kind (OBJECT, INPUT_OBJECT, ENUM, ...).",
|
|
96
|
+
),
|
|
97
|
+
rich: bool = typer.Option(
|
|
98
|
+
False, "--rich", help="Pretty-print with Rich instead of JSON."
|
|
99
|
+
),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Search schema types (``search_schema``)."""
|
|
102
|
+
|
|
103
|
+
async def factory(client: PipefyClient):
|
|
104
|
+
return await client.search_schema(keyword, kind=kind)
|
|
105
|
+
|
|
106
|
+
payload = run_pipefy_client_coroutine(ctx, factory)
|
|
107
|
+
_emit_introspection(payload, rich=rich)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
introspect_app.add_typer(schema_app, name="schema")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Pipe label 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
|
+
normalize_label_color_cli,
|
|
12
|
+
resource_id_argument,
|
|
13
|
+
run_cli_command,
|
|
14
|
+
validate_label_name_cli,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
label_app = typer.Typer(help="Pipe label operations.", no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@label_app.command("list")
|
|
21
|
+
def label_list(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
24
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""List labels on a pipe (from ``get_pipe``); JSON shape matches MCP ``get_labels``."""
|
|
27
|
+
|
|
28
|
+
async def factory(client: PipefyClient):
|
|
29
|
+
raw = await client.get_pipe(pipe_id)
|
|
30
|
+
pipe_node = (raw or {}).get("pipe")
|
|
31
|
+
if pipe_node is None:
|
|
32
|
+
return {"success": False, "error": "Pipe not found or access denied."}
|
|
33
|
+
labels = pipe_node.get("labels")
|
|
34
|
+
if labels is None:
|
|
35
|
+
labels = []
|
|
36
|
+
return {
|
|
37
|
+
"success": True,
|
|
38
|
+
"message": "Labels loaded.",
|
|
39
|
+
"labels": labels,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
run_cli_command(ctx, json_out, factory)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@label_app.command("create")
|
|
46
|
+
def label_create(
|
|
47
|
+
ctx: typer.Context,
|
|
48
|
+
pipe_id: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
49
|
+
name: str = typer.Option(..., "--name", "-n", help="Label name."),
|
|
50
|
+
color: str = typer.Option(
|
|
51
|
+
...,
|
|
52
|
+
"--color",
|
|
53
|
+
"-c",
|
|
54
|
+
help="Label color as hex #RRGGBB (e.g. #E50000, #FF0000).",
|
|
55
|
+
),
|
|
56
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Create a label on a pipe."""
|
|
59
|
+
|
|
60
|
+
nm = validate_label_name_cli(name)
|
|
61
|
+
clr = normalize_label_color_cli(color)
|
|
62
|
+
|
|
63
|
+
async def factory(client: PipefyClient):
|
|
64
|
+
return await client.create_label(pipe_id, nm, clr)
|
|
65
|
+
|
|
66
|
+
run_cli_command(ctx, json_out, factory)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@label_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
70
|
+
def label_update(
|
|
71
|
+
ctx: typer.Context,
|
|
72
|
+
label_id: str = resource_id_argument(help="Label id."),
|
|
73
|
+
name: str = typer.Option(
|
|
74
|
+
..., "--name", "-n", help="New label name (required by API)."
|
|
75
|
+
),
|
|
76
|
+
color: str = typer.Option(
|
|
77
|
+
...,
|
|
78
|
+
"--color",
|
|
79
|
+
"-c",
|
|
80
|
+
help="New label color as hex #RRGGBB (required by API).",
|
|
81
|
+
),
|
|
82
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Update a label (name and color are both required by Pipefy)."""
|
|
85
|
+
|
|
86
|
+
nm = validate_label_name_cli(name)
|
|
87
|
+
clr = normalize_label_color_cli(color)
|
|
88
|
+
|
|
89
|
+
async def factory(client: PipefyClient):
|
|
90
|
+
return await client.update_label(label_id, name=nm, color=clr)
|
|
91
|
+
|
|
92
|
+
run_cli_command(ctx, json_out, factory)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@label_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
96
|
+
def label_delete(
|
|
97
|
+
ctx: typer.Context,
|
|
98
|
+
label_id: str = resource_id_argument(help="Label id."),
|
|
99
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
100
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Delete a label permanently."""
|
|
103
|
+
|
|
104
|
+
confirm_destructive(yes=yes, description=f"label {label_id}")
|
|
105
|
+
|
|
106
|
+
async def factory(client: PipefyClient):
|
|
107
|
+
return await client.delete_label(label_id)
|
|
108
|
+
|
|
109
|
+
run_cli_command(ctx, json_out, factory)
|