pipecatcloud 0.6.0__tar.gz → 0.7.0__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.
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/CHANGELOG.md +22 -0
- pipecatcloud-0.7.0/CLAUDE.md +119 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/PKG-INFO +1 -1
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/pyproject.toml +0 -1
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/deploy_utils.py +15 -3
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/api.py +21 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/secrets.py +43 -11
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/SOURCES.txt +1 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_api_client.py +35 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_deployment_status.py +82 -4
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.claude/settings.json +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.gitignore +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/LICENSE +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/README.md +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/pytest.ini +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/setup.cfg +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/regions.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/agent.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/build.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/deploy.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/docker.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/__init__.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/conftest.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_auth.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_build_utils.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_config_file.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_max_session_duration.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_pat_auth.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/tests/test_regions.py +0 -0
- {pipecatcloud-0.6.0 → pipecatcloud-0.7.0}/uv.lock +0 -0
|
@@ -5,6 +5,28 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - 2026-05-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `pcc secrets list` now shows a `Status` column for each secret set
|
|
13
|
+
(`ready`, `pending`, or `failed`). When a single set is requested
|
|
14
|
+
(`pcc secrets list <name>`), the readiness status is shown in the panel
|
|
15
|
+
title alongside the keys, and a follow-up message explains pending or
|
|
16
|
+
failed states. Backed by a new `secrets_get` API client method that
|
|
17
|
+
fetches a single secret set's full payload, including readiness fields.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `pcc agent status` and the `pcc deploy` polling output now show
|
|
22
|
+
customer-friendly headlines for failed deployments when the API
|
|
23
|
+
supplies them. An OOM-killed container reads "Out of memory" instead
|
|
24
|
+
of "CrashLoopBackOff (OOMKilled, exit 137)"; an application crash
|
|
25
|
+
reads "Application Error" with the captured traceback on the next
|
|
26
|
+
line. Exit codes render as "exit code N" rather than "exit N" for
|
|
27
|
+
clarity. Older API responses without the new field continue to render
|
|
28
|
+
via the previous format.
|
|
29
|
+
|
|
8
30
|
## [0.6.0] - 2026-04-22
|
|
9
31
|
|
|
10
32
|
### Added
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for Claude Code working in this repo.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
**Pipecat Cloud** is the Python SDK and `pcc` CLI for deploying and managing Pipecat voice agents on Pipecat Cloud infrastructure. It ships to PyPI as `pipecatcloud`. Users install it to authenticate, build and push container images, deploy agents, manage secrets and scaling, and open sessions against running agents.
|
|
8
|
+
|
|
9
|
+
- Package root: `src/pipecatcloud/`
|
|
10
|
+
- CLI entry points: `pcc` and `pipecatcloud` (also registered as a Pipecat CLI extension via the `pipecat_cli.extensions:cloud` entry point)
|
|
11
|
+
- Public docs: https://docs.pipecat.daily.co
|
|
12
|
+
|
|
13
|
+
## Toolchain
|
|
14
|
+
|
|
15
|
+
- **Python 3.11+** (bumped from 3.10 in v0.5.0 to match Pipecat 1.0). Type hints use `X | Y` syntax, not `Union[X, Y]`.
|
|
16
|
+
- **uv** for dependency management. `uv.lock` is committed.
|
|
17
|
+
- **setuptools** build backend. Version is derived from git tags via `setuptools_scm` (tag format `v0.6.0`), so there is no hand-edited version file.
|
|
18
|
+
- **Ruff** for lint and format (100-char line length, `I` and `UP` rule sets).
|
|
19
|
+
- **Pyright** for type checking.
|
|
20
|
+
- **Pytest** with `pytest-asyncio` in `auto` mode.
|
|
21
|
+
|
|
22
|
+
## Common commands
|
|
23
|
+
|
|
24
|
+
Always use `uv run` rather than activating a venv.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# First-time setup
|
|
28
|
+
uv sync --group dev
|
|
29
|
+
uv run pre-commit install
|
|
30
|
+
|
|
31
|
+
# Tests
|
|
32
|
+
uv run pytest # full suite
|
|
33
|
+
uv run pytest tests/test_auth.py # single file
|
|
34
|
+
uv run pytest tests/test_auth.py::TestPKCE # single class
|
|
35
|
+
uv run pytest -v # verbose
|
|
36
|
+
|
|
37
|
+
# Lint / format / typecheck
|
|
38
|
+
uv run ruff format .
|
|
39
|
+
uv run ruff check --fix .
|
|
40
|
+
uv run pyright
|
|
41
|
+
bash scripts/fix-ruff.sh # format + check --fix
|
|
42
|
+
|
|
43
|
+
# Build (usually CI-only)
|
|
44
|
+
uv build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pre-commit runs `ruff format` and `ruff check --fix` on every commit. If the hook fails, fix the issue and make a **new** commit rather than amending.
|
|
48
|
+
|
|
49
|
+
## Repository layout
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/pipecatcloud/
|
|
53
|
+
__init__.py Public SDK surface (Session, exceptions, session argument types)
|
|
54
|
+
api.py Async aiohttp API client (_API class)
|
|
55
|
+
session.py User-facing Session client
|
|
56
|
+
agent.py Session argument types with fallbacks when pipecat-ai is absent
|
|
57
|
+
config.py Base settings, API path constants
|
|
58
|
+
constants.py Krisp VIVA models, region-related literals
|
|
59
|
+
exception.py Error hierarchy (AuthError, AgentStartError, etc.)
|
|
60
|
+
cli/
|
|
61
|
+
entry_point.py Typer app, wired as a Pipecat CLI extension
|
|
62
|
+
config.py Reads/writes ~/.config/pipecatcloud/pipecatcloud.toml
|
|
63
|
+
api.py CLI-flavored wrapper around _API
|
|
64
|
+
commands/ auth, agent, deploy, docker, build, secrets, regions, organizations
|
|
65
|
+
_utils/
|
|
66
|
+
auth_utils.py @requires_login decorator, default-org resolution
|
|
67
|
+
build_utils.py Deterministic tarballs, S3 upload, build polling
|
|
68
|
+
deploy_utils.py pcc-deploy.toml parsing, deployment-status interpretation
|
|
69
|
+
regions.py Region fetch and validation
|
|
70
|
+
async_utils.py synchronizer for bridging async into the sync Typer CLI
|
|
71
|
+
console_utils.py Rich console helpers
|
|
72
|
+
smallwebrtc/
|
|
73
|
+
session_manager.py SmallWebRTC support for non-Pipecat agents
|
|
74
|
+
tests/ Pytest suite, parallel to src/
|
|
75
|
+
.github/workflows/ tests.yml, format.yml, publish-pypi.yml, publish-test.yml
|
|
76
|
+
pcc-deploy.toml Example deployment config consumed by `pcc deploy`
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Testing conventions
|
|
80
|
+
|
|
81
|
+
- Layout mirrors `src/`. New tests go in `tests/test_<module>.py`.
|
|
82
|
+
- `tests/conftest.py` sets `PIPECAT_CONFIG_PATH` to an isolated temp file **before imports**, pre-populated with `token = "test-token"` and `org = "test-org"`. This prevents tests from reading or clobbering real credentials. Do not bypass it.
|
|
83
|
+
- Async tests use `@pytest.mark.asyncio`. Mock async calls with `unittest.mock.AsyncMock`.
|
|
84
|
+
- `tests/test_agent_sessions.py` is **skipped in CI** because it needs real auth or heavier mocking. Run it locally if you touch session code.
|
|
85
|
+
- `pytest.ini` uses `--import-mode=importlib`. Some tests manually insert `src/` onto `sys.path`, which is expected.
|
|
86
|
+
|
|
87
|
+
## CI
|
|
88
|
+
|
|
89
|
+
- `tests.yml` runs on push to main and PRs. It currently runs `uv run pytest tests/test_docker_commands.py -v` rather than the full suite, so green CI does not guarantee the whole suite passes. Run `uv run pytest` locally before opening PRs.
|
|
90
|
+
- `format.yml` runs `ruff format --diff` and `ruff check`. Both must pass.
|
|
91
|
+
- `publish-pypi.yml` and `publish-test.yml` are manual dispatches that build and publish a specific git tag.
|
|
92
|
+
|
|
93
|
+
## Config and environment
|
|
94
|
+
|
|
95
|
+
- User config lives at `~/.config/pipecatcloud/pipecatcloud.toml`. Overridable with `PIPECAT_CONFIG_PATH`.
|
|
96
|
+
- Deployment config is `pcc-deploy.toml` in the project root. Overridable with `PIPECAT_DEPLOY_CONFIG_PATH`.
|
|
97
|
+
- Other overrides follow the `PIPECAT_<UPPERCASE_SETTING>` pattern (e.g. `PIPECAT_API_HOST`).
|
|
98
|
+
- `PCC_LOG_LEVEL` controls the loguru log level (default `INFO`). This codebase uses **loguru**, not the stdlib `logging` module.
|
|
99
|
+
- The credentials file is force-chmod'd to `0600` on read and written atomically (temp file, fsync, rename). Preserve this when touching `cli/config.py`.
|
|
100
|
+
|
|
101
|
+
## Gotchas
|
|
102
|
+
|
|
103
|
+
- The CLI is sync (Typer) but the API client is async. Bridge with `@synchronizer` from `_utils/async_utils.py`. Do not invent a new bridging pattern.
|
|
104
|
+
- Auth uses OAuth2 with PKCE (RFC 7636). OAuth endpoints are discovered from the API server, not hardcoded. The callback server tries ports 8400-8404.
|
|
105
|
+
- `SmallWebRTCSessionArguments` and `SmallWebRTCRunnerArguments` have fallback definitions in `agent.py` for when `pipecat-ai` isn't installed. Keep the two paths in sync.
|
|
106
|
+
- Krisp VIVA audio filter names are `Literal["tel", "pro"]` in `constants.py`. These must match the server-side ConfigMap, so coordinate changes with the backend.
|
|
107
|
+
|
|
108
|
+
## Release flow
|
|
109
|
+
|
|
110
|
+
1. Update `CHANGELOG.md` following Keep a Changelog format.
|
|
111
|
+
2. Tag the commit with `v<MAJOR>.<MINOR>.<PATCH>`.
|
|
112
|
+
3. Trigger `publish-pypi.yml` manually with that tag. `setuptools_scm` picks up the version from the tag.
|
|
113
|
+
|
|
114
|
+
## Writing style for generated content in this repo
|
|
115
|
+
|
|
116
|
+
- No emdashes.
|
|
117
|
+
- Clear mid-length sentences. Short paragraphs.
|
|
118
|
+
- Bold for key terms on first use only.
|
|
119
|
+
- Don't hedge, don't hype, don't pad with "let's dive in" style openers.
|
|
@@ -104,19 +104,26 @@ def format_health_lines(health: dict) -> list:
|
|
|
104
104
|
"""Format health details as indented lines under a revision."""
|
|
105
105
|
lines = []
|
|
106
106
|
reason_parts = []
|
|
107
|
+
|
|
108
|
+
# Prefer the customer-friendly headline from the API when present. Older API
|
|
109
|
+
# responses don't include it, so fall back to constructing a string from raw
|
|
110
|
+
# k8s fields.
|
|
111
|
+
headline = health.get("headline")
|
|
107
112
|
reason = health.get("reason", "")
|
|
108
113
|
term_reason = health.get("lastTerminationReason", "")
|
|
109
114
|
exit_code = health.get("lastExitCode")
|
|
110
115
|
|
|
111
|
-
if
|
|
116
|
+
if headline:
|
|
117
|
+
reason_parts.append(f"[red]{headline}[/red]")
|
|
118
|
+
elif reason:
|
|
112
119
|
detail = reason
|
|
113
120
|
if term_reason and term_reason != reason:
|
|
114
121
|
detail += f" ({term_reason}"
|
|
115
122
|
if exit_code is not None:
|
|
116
|
-
detail += f", exit {exit_code}"
|
|
123
|
+
detail += f", exit code {exit_code}"
|
|
117
124
|
detail += ")"
|
|
118
125
|
elif exit_code is not None:
|
|
119
|
-
detail += f" (exit {exit_code})"
|
|
126
|
+
detail += f" (exit code {exit_code})"
|
|
120
127
|
reason_parts.append(f"[red]{detail}[/red]")
|
|
121
128
|
|
|
122
129
|
restarts = health.get("restartCount", 0)
|
|
@@ -124,6 +131,11 @@ def format_health_lines(health: dict) -> list:
|
|
|
124
131
|
if restarts > 0:
|
|
125
132
|
reason_parts.append(f"[dim]·[/dim] {restarts} restarts across {replicas_started} replicas")
|
|
126
133
|
|
|
134
|
+
# When using the headline, surface the exit code as its own subhead segment.
|
|
135
|
+
# The fallback path already includes the exit code inline with the reason.
|
|
136
|
+
if headline and exit_code is not None:
|
|
137
|
+
reason_parts.append(f"[dim]·[/dim] exit code {exit_code}")
|
|
138
|
+
|
|
127
139
|
if reason_parts:
|
|
128
140
|
lines.append(" " + " ".join(reason_parts))
|
|
129
141
|
|
|
@@ -354,6 +354,27 @@ class _API:
|
|
|
354
354
|
"""
|
|
355
355
|
return self.create_api_method(self._secrets_list)
|
|
356
356
|
|
|
357
|
+
async def _secrets_get(self, org: str, secret_set: str) -> dict | None:
|
|
358
|
+
"""Fetch a single secret set, including its readiness status.
|
|
359
|
+
|
|
360
|
+
Returns the full response object: {secrets, region, status, errorMessage?}.
|
|
361
|
+
Use this when you need readiness; use _secrets_list when you only need
|
|
362
|
+
the existence check or the flat keys array.
|
|
363
|
+
"""
|
|
364
|
+
url = f"{self.construct_api_url('secrets_path').format(org=org)}/{secret_set}"
|
|
365
|
+
result = await self._base_request("GET", url, not_found_is_empty=True)
|
|
366
|
+
return result or None
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def secrets_get(self):
|
|
370
|
+
"""Get a single secret set with readiness status.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
org: Organization ID
|
|
374
|
+
secret_set: Name of the secret set
|
|
375
|
+
"""
|
|
376
|
+
return self.create_api_method(self._secrets_get)
|
|
377
|
+
|
|
357
378
|
async def _secrets_upsert(
|
|
358
379
|
self, data: dict, set_name: str, org: str, region: str | None = None
|
|
359
380
|
) -> dict:
|
|
@@ -33,6 +33,17 @@ secrets_cli = typer.Typer(
|
|
|
33
33
|
# ---- Methods ----
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _format_status(status: str | None) -> str:
|
|
37
|
+
"""Render a readiness status with Rich styling. Unknown values pass through."""
|
|
38
|
+
if status == "ready":
|
|
39
|
+
return "[green]ready[/green]"
|
|
40
|
+
if status == "pending":
|
|
41
|
+
return "[yellow]pending[/yellow]"
|
|
42
|
+
if status == "failed":
|
|
43
|
+
return "[red]failed[/red]"
|
|
44
|
+
return status or "[dim]unknown[/dim]"
|
|
45
|
+
|
|
46
|
+
|
|
36
47
|
def validate_secrets(secrets: dict):
|
|
37
48
|
valid_name_pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
38
49
|
|
|
@@ -382,7 +393,10 @@ async def list(
|
|
|
382
393
|
logger.debug(f"Secret set name to lookup: {name}")
|
|
383
394
|
|
|
384
395
|
with console.status(f"[dim]{status_title}[/dim]", spinner="dots"):
|
|
385
|
-
|
|
396
|
+
if name:
|
|
397
|
+
data, error = await API.bubble_error().secrets_get(org=org, secret_set=name)
|
|
398
|
+
else:
|
|
399
|
+
data, error = await API.bubble_error().secrets_list(org=org, region=region)
|
|
386
400
|
|
|
387
401
|
if error:
|
|
388
402
|
if error == 400:
|
|
@@ -401,19 +415,27 @@ async def list(
|
|
|
401
415
|
return typer.Exit()
|
|
402
416
|
|
|
403
417
|
if name:
|
|
404
|
-
|
|
418
|
+
secrets = data.get("secrets") or []
|
|
419
|
+
status = data.get("status")
|
|
420
|
+
error_message = data.get("errorMessage")
|
|
405
421
|
|
|
406
422
|
table = Table(border_style="dim", show_header=False, show_edge=True, show_lines=True)
|
|
407
423
|
table.add_column(name, style="white")
|
|
408
|
-
for s in
|
|
424
|
+
for s in secrets:
|
|
409
425
|
table.add_row(s["fieldName"])
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
title_align="left",
|
|
415
|
-
)
|
|
426
|
+
|
|
427
|
+
title = (
|
|
428
|
+
f"[bold]Secret keys for set [green]{name}[/green][/bold] "
|
|
429
|
+
f"(status: {_format_status(status)})"
|
|
416
430
|
)
|
|
431
|
+
console.print(Panel(table, title=title, title_align="left"))
|
|
432
|
+
if status == "failed" and error_message:
|
|
433
|
+
console.print(f"[red]{error_message}[/red]")
|
|
434
|
+
elif status == "pending":
|
|
435
|
+
console.print(
|
|
436
|
+
"[yellow]This secret set is still being provisioned. "
|
|
437
|
+
"Deploys that bind it will be rejected until it is ready.[/yellow]"
|
|
438
|
+
)
|
|
417
439
|
else:
|
|
418
440
|
# Filter out image pull secrets if show all is False
|
|
419
441
|
filtered_sets = [s for s in data if show_all or s["type"] != "imagePullSecret"]
|
|
@@ -431,6 +453,7 @@ async def list(
|
|
|
431
453
|
)
|
|
432
454
|
table.add_column("Secret Set Name", style="white")
|
|
433
455
|
table.add_column("Region", style="white")
|
|
456
|
+
table.add_column("Status", style="white")
|
|
434
457
|
if show_all:
|
|
435
458
|
table.add_column("Type", style="white")
|
|
436
459
|
for secret_set in filtered_sets:
|
|
@@ -439,10 +462,19 @@ async def list(
|
|
|
439
462
|
if secret_set["type"] == "imagePullSecret"
|
|
440
463
|
else "Secret Set"
|
|
441
464
|
)
|
|
442
|
-
table.add_row(
|
|
465
|
+
table.add_row(
|
|
466
|
+
secret_set["name"],
|
|
467
|
+
secret_set["region"],
|
|
468
|
+
_format_status(secret_set.get("status")),
|
|
469
|
+
set_type,
|
|
470
|
+
)
|
|
443
471
|
else:
|
|
444
472
|
for secret_set in filtered_sets:
|
|
445
|
-
table.add_row(
|
|
473
|
+
table.add_row(
|
|
474
|
+
secret_set["name"],
|
|
475
|
+
secret_set["region"],
|
|
476
|
+
_format_status(secret_set.get("status")),
|
|
477
|
+
)
|
|
446
478
|
|
|
447
479
|
console.success(table, title_extra=f"Secret sets for {org}")
|
|
448
480
|
|
|
@@ -71,6 +71,41 @@ class TestAPISecretsRegions:
|
|
|
71
71
|
call_args = mock_request.call_args
|
|
72
72
|
assert call_args[1]["params"] == {"region": "ap"}
|
|
73
73
|
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_secrets_get_returns_full_response(self, api_client):
|
|
76
|
+
"""secrets_get returns the whole single-set response, including readiness fields."""
|
|
77
|
+
# Arrange
|
|
78
|
+
with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
|
|
79
|
+
mock_request.return_value = {
|
|
80
|
+
"secrets": [{"fieldName": "API_KEY"}],
|
|
81
|
+
"region": "us-west",
|
|
82
|
+
"status": "ready",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Act
|
|
86
|
+
result = await api_client._secrets_get(org="test-org", secret_set="my-secrets")
|
|
87
|
+
|
|
88
|
+
# Assert
|
|
89
|
+
mock_request.assert_called_once()
|
|
90
|
+
assert result == {
|
|
91
|
+
"secrets": [{"fieldName": "API_KEY"}],
|
|
92
|
+
"region": "us-west",
|
|
93
|
+
"status": "ready",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@pytest.mark.asyncio
|
|
97
|
+
async def test_secrets_get_returns_none_on_404(self, api_client):
|
|
98
|
+
"""secrets_get returns None when the set does not exist."""
|
|
99
|
+
# Arrange
|
|
100
|
+
with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
|
|
101
|
+
mock_request.return_value = None
|
|
102
|
+
|
|
103
|
+
# Act
|
|
104
|
+
result = await api_client._secrets_get(org="test-org", secret_set="missing")
|
|
105
|
+
|
|
106
|
+
# Assert
|
|
107
|
+
assert result is None
|
|
108
|
+
|
|
74
109
|
@pytest.mark.asyncio
|
|
75
110
|
async def test_secrets_upsert_with_region(self, api_client):
|
|
76
111
|
"""Secrets upsert with region should include region in payload."""
|
|
@@ -497,7 +497,7 @@ class TestHealthLines:
|
|
|
497
497
|
assert len(lines) >= 1
|
|
498
498
|
assert "CrashLoopBackOff" in lines[0]
|
|
499
499
|
assert "OOMKilled" in lines[0]
|
|
500
|
-
assert "exit 137" in lines[0]
|
|
500
|
+
assert "exit code 137" in lines[0]
|
|
501
501
|
assert "42 restarts" in lines[0]
|
|
502
502
|
assert "3 replicas" in lines[0]
|
|
503
503
|
|
|
@@ -511,7 +511,7 @@ class TestHealthLines:
|
|
|
511
511
|
}
|
|
512
512
|
)
|
|
513
513
|
assert "Error" in lines[0]
|
|
514
|
-
assert "exit 1" in lines[0]
|
|
514
|
+
assert "exit code 1" in lines[0]
|
|
515
515
|
assert "4 restarts" in lines[0]
|
|
516
516
|
|
|
517
517
|
def test_message_shows_last_line(self):
|
|
@@ -564,8 +564,86 @@ class TestHealthLines:
|
|
|
564
564
|
"replicasStarted": 1,
|
|
565
565
|
}
|
|
566
566
|
)
|
|
567
|
-
# Should show "Error (exit 1)" not "Error (Error, exit 1)"
|
|
568
|
-
assert "Error (exit 1)" in lines[0]
|
|
567
|
+
# Should show "Error (exit code 1)" not "Error (Error, exit code 1)"
|
|
568
|
+
assert "Error (exit code 1)" in lines[0]
|
|
569
|
+
|
|
570
|
+
def test_headline_replaces_reason_string(self):
|
|
571
|
+
"""When the API supplies a customer-friendly headline, render it instead of
|
|
572
|
+
the raw k8s reason. Exit code is appended as its own segment so it stays
|
|
573
|
+
visible (today's format folded the exit code into the reason string)."""
|
|
574
|
+
lines = format_health_lines(
|
|
575
|
+
{
|
|
576
|
+
"reason": "CrashLoopBackOff",
|
|
577
|
+
"lastTerminationReason": "OOMKilled",
|
|
578
|
+
"lastExitCode": 137,
|
|
579
|
+
"restartCount": 5,
|
|
580
|
+
"replicasStarted": 2,
|
|
581
|
+
"headline": "Out of memory",
|
|
582
|
+
}
|
|
583
|
+
)
|
|
584
|
+
assert "Out of memory" in lines[0]
|
|
585
|
+
# Raw k8s vocabulary should NOT appear in the rendered line when we have a headline.
|
|
586
|
+
assert "CrashLoopBackOff" not in lines[0]
|
|
587
|
+
assert "OOMKilled" not in lines[0]
|
|
588
|
+
# Restart count and exit code are both visible.
|
|
589
|
+
assert "5 restarts across 2 replicas" in lines[0]
|
|
590
|
+
assert "exit code 137" in lines[0]
|
|
591
|
+
|
|
592
|
+
def test_headline_with_traceback_message(self):
|
|
593
|
+
"""Application Error case: headline plus the last line of the captured traceback."""
|
|
594
|
+
lines = format_health_lines(
|
|
595
|
+
{
|
|
596
|
+
"reason": "CrashLoopBackOff",
|
|
597
|
+
"lastTerminationReason": "Error",
|
|
598
|
+
"lastExitCode": 1,
|
|
599
|
+
"restartCount": 3,
|
|
600
|
+
"replicasStarted": 2,
|
|
601
|
+
"headline": "Application Error",
|
|
602
|
+
"message": (
|
|
603
|
+
"Traceback (most recent call last):\n"
|
|
604
|
+
' File "bot.py"\n'
|
|
605
|
+
"ModuleNotFoundError: No module named 'nonexistentlibthiswillfail'"
|
|
606
|
+
),
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
assert len(lines) == 2
|
|
610
|
+
assert "Application Error" in lines[0]
|
|
611
|
+
assert "exit code 1" in lines[0]
|
|
612
|
+
assert "ModuleNotFoundError" in lines[1]
|
|
613
|
+
assert "Traceback" not in lines[1]
|
|
614
|
+
|
|
615
|
+
def test_headline_without_exit_code(self):
|
|
616
|
+
"""ImagePullBackOff and similar pre-start failures have no exit code; the
|
|
617
|
+
exit-code segment must be omitted, not rendered as 'exit None' or similar."""
|
|
618
|
+
lines = format_health_lines(
|
|
619
|
+
{
|
|
620
|
+
"reason": "ImagePullBackOff",
|
|
621
|
+
"restartCount": 0,
|
|
622
|
+
"replicasStarted": 2,
|
|
623
|
+
"headline": "Cannot pull image",
|
|
624
|
+
"message": 'Back-off pulling image "nonexistent:latest"',
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
assert "Cannot pull image" in lines[0]
|
|
628
|
+
assert "exit" not in lines[0]
|
|
629
|
+
assert "ImagePullBackOff" not in lines[0]
|
|
630
|
+
assert "nonexistent:latest" in lines[1]
|
|
631
|
+
|
|
632
|
+
def test_unmapped_reason_falls_back_to_raw_format(self):
|
|
633
|
+
"""An older API response (or a future unmapped reason) has no headline;
|
|
634
|
+
rendering must continue to work via the fallback path."""
|
|
635
|
+
lines = format_health_lines(
|
|
636
|
+
{
|
|
637
|
+
"reason": "CrashLoopBackOff",
|
|
638
|
+
"lastTerminationReason": "OOMKilled",
|
|
639
|
+
"lastExitCode": 137,
|
|
640
|
+
"restartCount": 5,
|
|
641
|
+
"replicasStarted": 2,
|
|
642
|
+
}
|
|
643
|
+
)
|
|
644
|
+
# Existing format preserved exactly: parens, comma, k8s vocabulary.
|
|
645
|
+
assert "CrashLoopBackOff (OOMKilled, exit code 137)" in lines[0]
|
|
646
|
+
assert "5 restarts across 2 replicas" in lines[0]
|
|
569
647
|
|
|
570
648
|
|
|
571
649
|
class TestRevisionLineWithHealth:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|