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.
Files changed (67) hide show
  1. pipefy_cli-0.3.0a1/.gitignore +56 -0
  2. pipefy_cli-0.3.0a1/PKG-INFO +104 -0
  3. pipefy_cli-0.3.0a1/README.md +92 -0
  4. pipefy_cli-0.3.0a1/pyproject.toml +54 -0
  5. pipefy_cli-0.3.0a1/src/pipefy_cli/__init__.py +7 -0
  6. pipefy_cli-0.3.0a1/src/pipefy_cli/_docs.py +3 -0
  7. pipefy_cli-0.3.0a1/src/pipefy_cli/auth.py +225 -0
  8. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/__init__.py +5 -0
  9. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/_common.py +359 -0
  10. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/agent.py +358 -0
  11. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/ai_automation.py +318 -0
  12. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/attachment.py +122 -0
  13. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/audit.py +63 -0
  14. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/auth.py +519 -0
  15. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/automation.py +529 -0
  16. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/card.py +439 -0
  17. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/email.py +169 -0
  18. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/export.py +61 -0
  19. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/field.py +113 -0
  20. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/field_condition.py +201 -0
  21. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/graphql.py +110 -0
  22. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/introspect.py +110 -0
  23. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/label.py +109 -0
  24. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/member.py +126 -0
  25. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/org.py +60 -0
  26. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/phase.py +264 -0
  27. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/pipe.py +167 -0
  28. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/portal.py +871 -0
  29. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/record.py +219 -0
  30. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/relation.py +173 -0
  31. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/report_org.py +241 -0
  32. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/report_pipe.py +290 -0
  33. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/table.py +151 -0
  34. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/usage.py +100 -0
  35. pipefy_cli-0.3.0a1/src/pipefy_cli/commands/webhook.py +141 -0
  36. pipefy_cli-0.3.0a1/src/pipefy_cli/main.py +132 -0
  37. pipefy_cli-0.3.0a1/src/pipefy_cli/output/__init__.py +8 -0
  38. pipefy_cli-0.3.0a1/src/pipefy_cli/output/json_renderer.py +24 -0
  39. pipefy_cli-0.3.0a1/src/pipefy_cli/output/rich_renderer.py +81 -0
  40. pipefy_cli-0.3.0a1/src/pipefy_cli/settings.py +66 -0
  41. pipefy_cli-0.3.0a1/tests/conftest.py +112 -0
  42. pipefy_cli-0.3.0a1/tests/fixtures/cli_help_golden.txt +3020 -0
  43. pipefy_cli-0.3.0a1/tests/test_auth.py +318 -0
  44. pipefy_cli-0.3.0a1/tests/test_auth_integration.py +37 -0
  45. pipefy_cli-0.3.0a1/tests/test_auth_login.py +828 -0
  46. pipefy_cli-0.3.0a1/tests/test_auth_logout.py +301 -0
  47. pipefy_cli-0.3.0a1/tests/test_auth_status.py +474 -0
  48. pipefy_cli-0.3.0a1/tests/test_card_commands.py +447 -0
  49. pipefy_cli-0.3.0a1/tests/test_card_get.py +132 -0
  50. pipefy_cli-0.3.0a1/tests/test_cli_agent_automation_smoke.py +790 -0
  51. pipefy_cli-0.3.0a1/tests/test_cli_domain_integration_smoke.py +137 -0
  52. pipefy_cli-0.3.0a1/tests/test_cli_help_golden.py +114 -0
  53. pipefy_cli-0.3.0a1/tests/test_cli_settings.py +267 -0
  54. pipefy_cli-0.3.0a1/tests/test_cli_settings_back_compat.py +75 -0
  55. pipefy_cli-0.3.0a1/tests/test_common_helpers.py +281 -0
  56. pipefy_cli-0.3.0a1/tests/test_completion.py +73 -0
  57. pipefy_cli-0.3.0a1/tests/test_label_webhook_commands.py +233 -0
  58. pipefy_cli-0.3.0a1/tests/test_org_commands.py +63 -0
  59. pipefy_cli-0.3.0a1/tests/test_output_renderers.py +136 -0
  60. pipefy_cli-0.3.0a1/tests/test_phase_field_commands.py +169 -0
  61. pipefy_cli-0.3.0a1/tests/test_pipe_commands.py +358 -0
  62. pipefy_cli-0.3.0a1/tests/test_portal_commands.py +1522 -0
  63. pipefy_cli-0.3.0a1/tests/test_relation_member_commands.py +146 -0
  64. pipefy_cli-0.3.0a1/tests/test_report_pipe_filter_commands.py +130 -0
  65. pipefy_cli-0.3.0a1/tests/test_table_record_commands.py +190 -0
  66. pipefy_cli-0.3.0a1/tests/test_v8_v9_cli_commands.py +551 -0
  67. 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,7 @@
1
+ """Typer CLI entry surface for Pipefy (pipefy)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.3.0-alpha.1"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ DOCS_CLI_AUTH_REF = "docs/cli/auth.md"
@@ -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
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from pipefy_cli.commands.card import card_app
4
+
5
+ __all__ = ["card_app"]