pipefy-cli 0.3.0a1__tar.gz
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-0.3.0a1/.gitignore +56 -0
- pipefy_cli-0.3.0a1/PKG-INFO +104 -0
- pipefy_cli-0.3.0a1/README.md +92 -0
- pipefy_cli-0.3.0a1/pyproject.toml +54 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/__init__.py +7 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/_docs.py +3 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/auth.py +225 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/__init__.py +5 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/_common.py +359 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/agent.py +358 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/ai_automation.py +318 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/attachment.py +122 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/audit.py +63 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/auth.py +519 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/automation.py +529 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/card.py +439 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/email.py +169 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/export.py +61 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/field.py +113 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/field_condition.py +201 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/graphql.py +110 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/introspect.py +110 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/label.py +109 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/member.py +126 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/org.py +60 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/phase.py +264 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/pipe.py +167 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/portal.py +871 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/record.py +219 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/relation.py +173 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/report_org.py +241 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/report_pipe.py +290 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/table.py +151 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/usage.py +100 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/commands/webhook.py +141 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/main.py +132 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/output/__init__.py +8 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/output/json_renderer.py +24 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/output/rich_renderer.py +81 -0
- pipefy_cli-0.3.0a1/src/pipefy_cli/settings.py +66 -0
- pipefy_cli-0.3.0a1/tests/conftest.py +112 -0
- pipefy_cli-0.3.0a1/tests/fixtures/cli_help_golden.txt +3020 -0
- pipefy_cli-0.3.0a1/tests/test_auth.py +318 -0
- pipefy_cli-0.3.0a1/tests/test_auth_integration.py +37 -0
- pipefy_cli-0.3.0a1/tests/test_auth_login.py +828 -0
- pipefy_cli-0.3.0a1/tests/test_auth_logout.py +301 -0
- pipefy_cli-0.3.0a1/tests/test_auth_status.py +474 -0
- pipefy_cli-0.3.0a1/tests/test_card_commands.py +447 -0
- pipefy_cli-0.3.0a1/tests/test_card_get.py +132 -0
- pipefy_cli-0.3.0a1/tests/test_cli_agent_automation_smoke.py +790 -0
- pipefy_cli-0.3.0a1/tests/test_cli_domain_integration_smoke.py +137 -0
- pipefy_cli-0.3.0a1/tests/test_cli_help_golden.py +114 -0
- pipefy_cli-0.3.0a1/tests/test_cli_settings.py +267 -0
- pipefy_cli-0.3.0a1/tests/test_cli_settings_back_compat.py +75 -0
- pipefy_cli-0.3.0a1/tests/test_common_helpers.py +281 -0
- pipefy_cli-0.3.0a1/tests/test_completion.py +73 -0
- pipefy_cli-0.3.0a1/tests/test_label_webhook_commands.py +233 -0
- pipefy_cli-0.3.0a1/tests/test_org_commands.py +63 -0
- pipefy_cli-0.3.0a1/tests/test_output_renderers.py +136 -0
- pipefy_cli-0.3.0a1/tests/test_phase_field_commands.py +169 -0
- pipefy_cli-0.3.0a1/tests/test_pipe_commands.py +358 -0
- pipefy_cli-0.3.0a1/tests/test_portal_commands.py +1522 -0
- pipefy_cli-0.3.0a1/tests/test_relation_member_commands.py +146 -0
- pipefy_cli-0.3.0a1/tests/test_report_pipe_filter_commands.py +130 -0
- pipefy_cli-0.3.0a1/tests/test_table_record_commands.py +190 -0
- pipefy_cli-0.3.0a1/tests/test_v8_v9_cli_commands.py +551 -0
- pipefy_cli-0.3.0a1/tests/test_version_flag.py +18 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Ruff stuff:
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
|
|
12
|
+
# Unit test / coverage reports
|
|
13
|
+
htmlcov/
|
|
14
|
+
.tox/
|
|
15
|
+
.nox/
|
|
16
|
+
.coverage
|
|
17
|
+
.coverage.*
|
|
18
|
+
.cache
|
|
19
|
+
nosetests.xml
|
|
20
|
+
coverage.xml
|
|
21
|
+
*.cover
|
|
22
|
+
*.py,cover
|
|
23
|
+
.hypothesis/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
cover/
|
|
26
|
+
|
|
27
|
+
# Environments
|
|
28
|
+
.env
|
|
29
|
+
.venv
|
|
30
|
+
env/
|
|
31
|
+
venv/
|
|
32
|
+
ENV/
|
|
33
|
+
env.bak/
|
|
34
|
+
venv.bak/
|
|
35
|
+
.mise.toml
|
|
36
|
+
|
|
37
|
+
# Ralph Wiggum (local autonomous runs; state must not land in git)
|
|
38
|
+
.ralph/
|
|
39
|
+
RALPH_TASK.md
|
|
40
|
+
|
|
41
|
+
# IDEs and editors
|
|
42
|
+
.cursor/
|
|
43
|
+
.claude/
|
|
44
|
+
# Cursor MCP workspace config (machine-specific paths or local server entries).
|
|
45
|
+
# Carve-out: repo-root .mcp.json is the Claude Code plugin's MCP server config
|
|
46
|
+
# (auto-discovered at plugin root) and is intentionally tracked.
|
|
47
|
+
.mcp.json
|
|
48
|
+
!/.mcp.json
|
|
49
|
+
|
|
50
|
+
# Tracked release tooling only (ignore ad-hoc local scripts and smoke artifacts)
|
|
51
|
+
scripts/*
|
|
52
|
+
!scripts/bump_version.py
|
|
53
|
+
|
|
54
|
+
# macOS
|
|
55
|
+
.DS_Store
|
|
56
|
+
**/.DS_Store
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipefy-cli
|
|
3
|
+
Version: 0.3.0a1
|
|
4
|
+
Summary: Typer CLI for Pipefy (pipefy).
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: pipefy
|
|
7
|
+
Requires-Dist: pipefy-auth
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.8.1
|
|
9
|
+
Requires-Dist: rich>=13
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# pipefy-cli
|
|
14
|
+
|
|
15
|
+
Typer-based CLI for Pipefy. Exposes all MCP tool capabilities as terminal commands and scripts. Depends on [`pipefy`](../sdk/README.md) for GraphQL calls.
|
|
16
|
+
|
|
17
|
+
## Install (pre-launch, v0.1 → v0.5)
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
uvx \
|
|
21
|
+
--with "pipefy @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/sdk" \
|
|
22
|
+
--with "pipefy-auth @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/auth" \
|
|
23
|
+
--from "git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/cli" \
|
|
24
|
+
--refresh pipefy-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or persistently:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv tool install \
|
|
31
|
+
--with "pipefy @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/sdk" \
|
|
32
|
+
--with "pipefy-auth @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/auth" \
|
|
33
|
+
"git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/cli"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `--with` flags are required pre-1.0 because the workspace members are not yet on PyPI. At v1.0 this collapses to `uv tool install pipefy-cli`.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Show all commands
|
|
42
|
+
pipefy --help
|
|
43
|
+
|
|
44
|
+
# Card operations
|
|
45
|
+
pipefy card get 12345 --json
|
|
46
|
+
pipefy card list --pipe 67890
|
|
47
|
+
pipefy card create --pipe 67890 --title "New card"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Agent skills are installed separately via [`skills.sh`](https://github.com/vercel-labs/skills); see [`skills/README.md`](../../skills/README.md).
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
Same `PIPEFY_*` environment variables as `pipefy-mcp-server` (`.env` in CWD is loaded automatically):
|
|
55
|
+
|
|
56
|
+
```env
|
|
57
|
+
PIPEFY_SERVICE_ACCOUNT_CLIENT_ID=your_client_id
|
|
58
|
+
PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET=your_client_secret
|
|
59
|
+
# Non-prod environments only:
|
|
60
|
+
# PIPEFY_BASE_URL=https://<your-api-host>
|
|
61
|
+
# PIPEFY_AUTH_URL=https://<your-signin-host>/realms/<realm>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`PIPEFY_BASE_URL` defaults to `https://app.pipefy.com` (drives the four API endpoints) and `PIPEFY_AUTH_URL` defaults to `https://signin.pipefy.com/realms/pipefy` (the OIDC issuer). Set them only for non-prod environments.
|
|
65
|
+
|
|
66
|
+
### Authentication paths
|
|
67
|
+
|
|
68
|
+
Three credential sources, in CLI precedence order:
|
|
69
|
+
|
|
70
|
+
1. **Interactive (`pipefy auth login`)** — browser OAuth flow, session stored in the OS keychain. Best for human developers. Status and revocation via `pipefy auth status` and `pipefy auth logout`.
|
|
71
|
+
2. **Static bearer (`PIPEFY_TOKEN` or `--token`)** — direct bearer token, no OAuth. Intended for CI and scripted use. Overrides everything else.
|
|
72
|
+
3. **Service-account OAuth (`PIPEFY_SERVICE_ACCOUNT_CLIENT_ID` + `PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET`)** — unattended OAuth client-credentials grant. Used by the MCP server.
|
|
73
|
+
|
|
74
|
+
Full env-var reference, validation rules, and `config.toml` precedence: [`docs/config.md`](../../docs/config.md). Auth deep-dive (precedence rules, troubleshooting, keychain backends): [`docs/cli/auth.md`](../../docs/cli/auth.md).
|
|
75
|
+
|
|
76
|
+
## Output modes
|
|
77
|
+
|
|
78
|
+
Every command defaults to **Rich-formatted** human output. Add `--json` for machine-readable JSON to stdout.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pipefy card get 12345 --json | jq '.title'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Parity with MCP
|
|
85
|
+
|
|
86
|
+
Every MCP tool has a CLI counterpart (or a tracked deferral). See [`docs/parity.md`](../../docs/parity.md) for the full matrix.
|
|
87
|
+
|
|
88
|
+
## Shell completion
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pipefy --install-completion bash # or zsh, fish, etc.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
From the **repository root**:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv sync
|
|
100
|
+
uv run pytest packages/cli/tests # CLI tests
|
|
101
|
+
uv run ruff check packages/cli/src # lint
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See [`AGENTS.md`](../../AGENTS.md) and [`CLAUDE.md`](../../CLAUDE.md) for contributor guidance.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# pipefy-cli
|
|
2
|
+
|
|
3
|
+
Typer-based CLI for Pipefy. Exposes all MCP tool capabilities as terminal commands and scripts. Depends on [`pipefy`](../sdk/README.md) for GraphQL calls.
|
|
4
|
+
|
|
5
|
+
## Install (pre-launch, v0.1 → v0.5)
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uvx \
|
|
9
|
+
--with "pipefy @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/sdk" \
|
|
10
|
+
--with "pipefy-auth @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/auth" \
|
|
11
|
+
--from "git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/cli" \
|
|
12
|
+
--refresh pipefy-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or persistently:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
uv tool install \
|
|
19
|
+
--with "pipefy @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/sdk" \
|
|
20
|
+
--with "pipefy-auth @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/auth" \
|
|
21
|
+
"git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/cli"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The `--with` flags are required pre-1.0 because the workspace members are not yet on PyPI. At v1.0 this collapses to `uv tool install pipefy-cli`.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Show all commands
|
|
30
|
+
pipefy --help
|
|
31
|
+
|
|
32
|
+
# Card operations
|
|
33
|
+
pipefy card get 12345 --json
|
|
34
|
+
pipefy card list --pipe 67890
|
|
35
|
+
pipefy card create --pipe 67890 --title "New card"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Agent skills are installed separately via [`skills.sh`](https://github.com/vercel-labs/skills); see [`skills/README.md`](../../skills/README.md).
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
Same `PIPEFY_*` environment variables as `pipefy-mcp-server` (`.env` in CWD is loaded automatically):
|
|
43
|
+
|
|
44
|
+
```env
|
|
45
|
+
PIPEFY_SERVICE_ACCOUNT_CLIENT_ID=your_client_id
|
|
46
|
+
PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET=your_client_secret
|
|
47
|
+
# Non-prod environments only:
|
|
48
|
+
# PIPEFY_BASE_URL=https://<your-api-host>
|
|
49
|
+
# PIPEFY_AUTH_URL=https://<your-signin-host>/realms/<realm>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`PIPEFY_BASE_URL` defaults to `https://app.pipefy.com` (drives the four API endpoints) and `PIPEFY_AUTH_URL` defaults to `https://signin.pipefy.com/realms/pipefy` (the OIDC issuer). Set them only for non-prod environments.
|
|
53
|
+
|
|
54
|
+
### Authentication paths
|
|
55
|
+
|
|
56
|
+
Three credential sources, in CLI precedence order:
|
|
57
|
+
|
|
58
|
+
1. **Interactive (`pipefy auth login`)** — browser OAuth flow, session stored in the OS keychain. Best for human developers. Status and revocation via `pipefy auth status` and `pipefy auth logout`.
|
|
59
|
+
2. **Static bearer (`PIPEFY_TOKEN` or `--token`)** — direct bearer token, no OAuth. Intended for CI and scripted use. Overrides everything else.
|
|
60
|
+
3. **Service-account OAuth (`PIPEFY_SERVICE_ACCOUNT_CLIENT_ID` + `PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET`)** — unattended OAuth client-credentials grant. Used by the MCP server.
|
|
61
|
+
|
|
62
|
+
Full env-var reference, validation rules, and `config.toml` precedence: [`docs/config.md`](../../docs/config.md). Auth deep-dive (precedence rules, troubleshooting, keychain backends): [`docs/cli/auth.md`](../../docs/cli/auth.md).
|
|
63
|
+
|
|
64
|
+
## Output modes
|
|
65
|
+
|
|
66
|
+
Every command defaults to **Rich-formatted** human output. Add `--json` for machine-readable JSON to stdout.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipefy card get 12345 --json | jq '.title'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Parity with MCP
|
|
73
|
+
|
|
74
|
+
Every MCP tool has a CLI counterpart (or a tracked deferral). See [`docs/parity.md`](../../docs/parity.md) for the full matrix.
|
|
75
|
+
|
|
76
|
+
## Shell completion
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pipefy --install-completion bash # or zsh, fish, etc.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
From the **repository root**:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync
|
|
88
|
+
uv run pytest packages/cli/tests # CLI tests
|
|
89
|
+
uv run ruff check packages/cli/src # lint
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
See [`AGENTS.md`](../../AGENTS.md) and [`CLAUDE.md`](../../CLAUDE.md) for contributor guidance.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pipefy-cli"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Typer CLI for Pipefy (pipefy)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pipefy",
|
|
9
|
+
"pipefy-auth",
|
|
10
|
+
"typer>=0.12",
|
|
11
|
+
"rich>=13",
|
|
12
|
+
"pydantic-settings>=2.8.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
pipefy = "pipefy_cli.main:app"
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
pipefy = { workspace = true }
|
|
20
|
+
pipefy-auth = { workspace = true }
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.3.5",
|
|
25
|
+
"pytest-asyncio>=0.26.0",
|
|
26
|
+
"ruff>=0.11.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/pipefy_cli"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version]
|
|
37
|
+
path = "src/pipefy_cli/__init__.py"
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
pythonpath = ["src", "tests", "../sdk/tests"]
|
|
42
|
+
markers = [
|
|
43
|
+
"unit: marks a test as a unit test",
|
|
44
|
+
"integration: marks tests that call the live Pipefy API",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
extend = "../../pyproject.toml"
|
|
49
|
+
lint.extend-select = ["TID251"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
52
|
+
"pipefy_mcp".msg = "pipefy_cli must not import the MCP package; use pipefy_sdk instead."
|
|
53
|
+
"pipefy_sdk.services".msg = "Import from pipefy_sdk only; pipefy_sdk.services is private."
|
|
54
|
+
"pipefy_sdk.queries".msg = "Import from pipefy_sdk only; pipefy_sdk.queries is private."
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Build an authenticated :class:`PipefyClient` from CLI configuration.
|
|
2
|
+
|
|
3
|
+
The precedence chain lives in :func:`pipefy_auth.resolve_pipefy_auth`; this
|
|
4
|
+
module collapses the CLI's two static-token surfaces (``--token`` flag and
|
|
5
|
+
``PIPEFY_TOKEN`` env var) into a single value before calling the resolver, and
|
|
6
|
+
translates resolver failures into Typer exits. The flag-vs-env distinction
|
|
7
|
+
survives only as a diagnostic label for ``pipefy auth status``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from typing import Any, Final, Literal, assert_never
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from pipefy_auth import (
|
|
19
|
+
OidcClient,
|
|
20
|
+
RefreshError,
|
|
21
|
+
ResolvedAuth,
|
|
22
|
+
ServiceAccount,
|
|
23
|
+
ServiceAccountAuth,
|
|
24
|
+
StaticTokenAuth,
|
|
25
|
+
StoredSessionAuth,
|
|
26
|
+
build_httpx_auth,
|
|
27
|
+
detect_pipefy_auth_methods,
|
|
28
|
+
ensure_fresh_session,
|
|
29
|
+
missing_auth_message,
|
|
30
|
+
resolve_pipefy_auth,
|
|
31
|
+
)
|
|
32
|
+
from pipefy_sdk import (
|
|
33
|
+
PipefyClient,
|
|
34
|
+
PipefySettings,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from pipefy_cli._docs import DOCS_CLI_AUTH_REF
|
|
38
|
+
|
|
39
|
+
# Display labels for ``pipefy auth status``. The resolver knows the
|
|
40
|
+
# static-token method; the CLI restores the flag-vs-env distinction here.
|
|
41
|
+
FLAG_TOKEN_SOURCE: Final = "flag-token"
|
|
42
|
+
ENV_TOKEN_SOURCE: Final = "env-token"
|
|
43
|
+
|
|
44
|
+
# Locked JSON wire schema for ``pipefy auth status``: the auth-method names,
|
|
45
|
+
# with the static-token method split into the CLI's flag-vs-env surfaces, plus
|
|
46
|
+
# an explicit ``"none"`` sentinel for when no method resolved. Produced by
|
|
47
|
+
# :func:`detect_cli_auth_methods` / :func:`to_display_source`; ``commands.auth``
|
|
48
|
+
# renders it.
|
|
49
|
+
DisplaySource = Literal[
|
|
50
|
+
"flag-token",
|
|
51
|
+
"env-token",
|
|
52
|
+
"service-account",
|
|
53
|
+
"stored-session",
|
|
54
|
+
"none",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class BearerToken:
|
|
60
|
+
"""Static bearer token plus the surface that produced it (``--token`` or env)."""
|
|
61
|
+
|
|
62
|
+
value: str = field(repr=False)
|
|
63
|
+
source: Literal["flag", "env"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class AuthContext:
|
|
68
|
+
"""Auth inputs for a single CLI invocation.
|
|
69
|
+
|
|
70
|
+
Each field maps to one auth method (bearer-token, service-account,
|
|
71
|
+
stored-session). Built once at startup from the loaded
|
|
72
|
+
:class:`pipefy_auth.AuthSettings` plus the per-invocation ``--token`` /
|
|
73
|
+
``PIPEFY_TOKEN`` resolution.
|
|
74
|
+
|
|
75
|
+
``oidc_client`` is ``None`` only when ``AuthSettings.disable_stored_session``
|
|
76
|
+
is set (env: PIPEFY_DISABLE_STORED_SESSION); the stored-session method is
|
|
77
|
+
then skipped end-to-end. Otherwise ``auth_url`` defaults to the prod IdP
|
|
78
|
+
and the client is always present.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
bearer_token: BearerToken | None
|
|
82
|
+
service_account: ServiceAccount | None
|
|
83
|
+
oidc_client: OidcClient | None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_cached_signature: str | None = None
|
|
87
|
+
# One-shot CLIs reuse this; long-lived programmatic use should call
|
|
88
|
+
# ``clear_authenticated_client_cache`` between logical sessions (tests reset via fixture).
|
|
89
|
+
_cached_client: PipefyClient | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def clear_authenticated_client_cache() -> None:
|
|
93
|
+
"""Drop the in-process client cache (tests and rare reload scenarios)."""
|
|
94
|
+
global _cached_signature, _cached_client
|
|
95
|
+
_cached_signature = None
|
|
96
|
+
_cached_client = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _resolver_kwargs(auth: AuthContext) -> dict[str, Any]:
|
|
100
|
+
"""Map an :class:`AuthContext` onto the keyword inputs the resolver takes."""
|
|
101
|
+
return {
|
|
102
|
+
"static_token": auth.bearer_token.value if auth.bearer_token else None,
|
|
103
|
+
"service_account": auth.service_account,
|
|
104
|
+
"oidc_client": auth.oidc_client,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve(auth: AuthContext) -> ResolvedAuth | None:
|
|
109
|
+
return resolve_pipefy_auth(**_resolver_kwargs(auth))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def to_display_source(
|
|
113
|
+
resolved: ResolvedAuth, bearer: BearerToken | None
|
|
114
|
+
) -> DisplaySource:
|
|
115
|
+
"""Map a resolved auth method to its locked ``auth status`` wire value.
|
|
116
|
+
|
|
117
|
+
The static-token method splits into the flag-vs-env distinction the CLI
|
|
118
|
+
surfaces; the other methods map to their resolver wire name unchanged.
|
|
119
|
+
"""
|
|
120
|
+
match resolved:
|
|
121
|
+
case StaticTokenAuth():
|
|
122
|
+
return (
|
|
123
|
+
FLAG_TOKEN_SOURCE
|
|
124
|
+
if bearer and bearer.source == "flag"
|
|
125
|
+
else ENV_TOKEN_SOURCE
|
|
126
|
+
)
|
|
127
|
+
case ServiceAccountAuth():
|
|
128
|
+
return "service-account"
|
|
129
|
+
case StoredSessionAuth():
|
|
130
|
+
return "stored-session"
|
|
131
|
+
case _:
|
|
132
|
+
assert_never(resolved)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def detect_cli_auth_methods(auth: AuthContext) -> list[ResolvedAuth]:
|
|
136
|
+
"""Return the detected auth methods for a CLI invocation, precedence-first.
|
|
137
|
+
|
|
138
|
+
The non-short-circuiting view of the chain: every configured method, not
|
|
139
|
+
just the winner. ``pipefy auth status`` renders each via
|
|
140
|
+
:func:`to_display_source` and treats the first as the active source.
|
|
141
|
+
"""
|
|
142
|
+
return detect_pipefy_auth_methods(**_resolver_kwargs(auth))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cache_key(
|
|
146
|
+
pipefy_settings: PipefySettings,
|
|
147
|
+
auth: AuthContext,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""SHA-256 digest of every input that could change the cached client.
|
|
150
|
+
|
|
151
|
+
Hashed (not stored as plaintext) so the dump's secrets — the bearer
|
|
152
|
+
token, the service-account ``client_secret`` — don't linger in module
|
|
153
|
+
state for the process lifetime. Adding a new field to
|
|
154
|
+
:class:`PipefySettings` or :class:`AuthContext` automatically participates
|
|
155
|
+
in the key without touching this function. The resolved method is omitted:
|
|
156
|
+
it is a pure function of the auth fields above, so it adds no distinction.
|
|
157
|
+
"""
|
|
158
|
+
payload = json.dumps(
|
|
159
|
+
{
|
|
160
|
+
"settings": pipefy_settings.model_dump(mode="json"),
|
|
161
|
+
"auth": asdict(auth),
|
|
162
|
+
},
|
|
163
|
+
sort_keys=True,
|
|
164
|
+
default=str,
|
|
165
|
+
)
|
|
166
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_authenticated_client(
|
|
170
|
+
pipefy_settings: PipefySettings,
|
|
171
|
+
auth: AuthContext,
|
|
172
|
+
) -> PipefyClient:
|
|
173
|
+
"""Return a facade client using the highest-precedence available auth source.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
typer.Exit: Code 2 when no auth source resolves, or when refreshing a
|
|
177
|
+
stored session fails.
|
|
178
|
+
"""
|
|
179
|
+
global _cached_signature, _cached_client
|
|
180
|
+
|
|
181
|
+
resolved = _resolve(auth)
|
|
182
|
+
if resolved is None:
|
|
183
|
+
typer.echo(f"{missing_auth_message()} See {DOCS_CLI_AUTH_REF}.", err=True)
|
|
184
|
+
raise typer.Exit(2)
|
|
185
|
+
|
|
186
|
+
# Stored-session: warm up eagerly so refresh failures surface as a clean
|
|
187
|
+
# exit(2) with a "run `pipefy auth login` again" hint instead of leaking
|
|
188
|
+
# out as a transport error on the first GraphQL call.
|
|
189
|
+
if isinstance(resolved, StoredSessionAuth):
|
|
190
|
+
try:
|
|
191
|
+
ensure_fresh_session(
|
|
192
|
+
issuer=resolved.oidc_client.issuer_url,
|
|
193
|
+
client_id=resolved.oidc_client.client_id,
|
|
194
|
+
)
|
|
195
|
+
except RefreshError as exc:
|
|
196
|
+
typer.echo(
|
|
197
|
+
f"Stored Pipefy session could not be refreshed: {exc}. "
|
|
198
|
+
"Run `pipefy auth login` to sign in again.",
|
|
199
|
+
err=True,
|
|
200
|
+
)
|
|
201
|
+
raise typer.Exit(2) from exc
|
|
202
|
+
|
|
203
|
+
key = _cache_key(pipefy_settings, auth)
|
|
204
|
+
if _cached_client is not None and _cached_signature == key:
|
|
205
|
+
return _cached_client
|
|
206
|
+
|
|
207
|
+
client = PipefyClient(
|
|
208
|
+
pipefy_settings, auth=build_httpx_auth(resolved), surface="cli"
|
|
209
|
+
)
|
|
210
|
+
_cached_signature = key
|
|
211
|
+
_cached_client = client
|
|
212
|
+
return client
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
__all__ = [
|
|
216
|
+
"AuthContext",
|
|
217
|
+
"BearerToken",
|
|
218
|
+
"DisplaySource",
|
|
219
|
+
"ENV_TOKEN_SOURCE",
|
|
220
|
+
"FLAG_TOKEN_SOURCE",
|
|
221
|
+
"clear_authenticated_client_cache",
|
|
222
|
+
"detect_cli_auth_methods",
|
|
223
|
+
"get_authenticated_client",
|
|
224
|
+
"to_display_source",
|
|
225
|
+
]
|