pipecatcloud 0.6.0__tar.gz → 0.7.1__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 (70) hide show
  1. pipecatcloud-0.7.1/.github/workflows/python-compatibility.yml +45 -0
  2. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/CHANGELOG.md +37 -0
  3. pipecatcloud-0.7.1/CLAUDE.md +119 -0
  4. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/PKG-INFO +1 -1
  5. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/pyproject.toml +0 -1
  6. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/deploy_utils.py +15 -3
  7. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/api.py +21 -0
  8. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/agent.py +1 -1
  9. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/organizations.py +1 -1
  10. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/secrets.py +45 -13
  11. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
  12. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud.egg-info/SOURCES.txt +2 -0
  13. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_api_client.py +35 -0
  14. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_deployment_status.py +82 -4
  15. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_regions.py +1 -1
  16. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.claude/settings.json +0 -0
  17. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.github/workflows/format.yml +0 -0
  18. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.github/workflows/publish-pypi.yml +0 -0
  19. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.github/workflows/publish-test.yml +0 -0
  20. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.github/workflows/tests.yml +0 -0
  21. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.gitignore +0 -0
  22. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/.pre-commit-config.yaml +0 -0
  23. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/LICENSE +0 -0
  24. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/README.md +0 -0
  25. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/pipecat-cloud.png +0 -0
  26. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/pytest.ini +0 -0
  27. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/scripts/fix-ruff.sh +0 -0
  28. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/setup.cfg +0 -0
  29. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/__init__.py +0 -0
  30. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/__main__.py +0 -0
  31. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/__version__.py +0 -0
  32. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/__init__.py +0 -0
  33. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/async_utils.py +0 -0
  34. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  35. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/build_utils.py +0 -0
  36. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/console_utils.py +0 -0
  37. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/_utils/regions.py +0 -0
  38. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/agent.py +0 -0
  39. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/__init__.py +0 -0
  40. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/api.py +0 -0
  41. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  42. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/auth.py +0 -0
  43. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/build.py +0 -0
  44. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/deploy.py +0 -0
  45. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/docker.py +0 -0
  46. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/commands/regions.py +0 -0
  47. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/config.py +0 -0
  48. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/cli/entry_point.py +0 -0
  49. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/config.py +0 -0
  50. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/constants.py +0 -0
  51. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/exception.py +0 -0
  52. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/py.typed +0 -0
  53. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/session.py +0 -0
  54. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  55. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  56. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  57. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud.egg-info/requires.txt +0 -0
  58. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  59. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/__init__.py +0 -0
  60. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/conftest.py +0 -0
  61. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_agent_sessions.py +0 -0
  62. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_agent_stop.py +0 -0
  63. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_auth.py +0 -0
  64. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_build_utils.py +0 -0
  65. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_config_file.py +0 -0
  66. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_docker_commands.py +0 -0
  67. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_krisp_viva.py +0 -0
  68. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_max_session_duration.py +0 -0
  69. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/tests/test_pat_auth.py +0 -0
  70. {pipecatcloud-0.6.0 → pipecatcloud-0.7.1}/uv.lock +0 -0
@@ -0,0 +1,45 @@
1
+ name: python-compatibility
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - main
8
+ pull_request:
9
+ branches:
10
+ - '**'
11
+
12
+ concurrency:
13
+ group: python-compat-${{ github.event.pull_request.number || github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ compat:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ python-version: ['3.11', '3.12', '3.13', '3.14']
23
+
24
+ name: Python ${{ matrix.python-version }}
25
+ steps:
26
+ - name: Checkout repo
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v3
31
+ with:
32
+ version: 'latest'
33
+
34
+ - name: Set up Python ${{ matrix.python-version }}
35
+ run: |
36
+ uv python install ${{ matrix.python-version }}
37
+ uv python pin ${{ matrix.python-version }}
38
+
39
+ - name: Install dependencies
40
+ run: uv sync --group dev
41
+
42
+ - name: Verify CLI loads
43
+ run: |
44
+ uv run python --version
45
+ uv run pcc --help
@@ -5,6 +5,43 @@ 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.1] - 2026-05-14
9
+
10
+ ### Fixed
11
+
12
+ - The `pcc` CLI now imports cleanly under Python 3.14. Previously, two
13
+ Typer command callbacks were defined with the names `list` and `set`,
14
+ shadowing the builtins. Under PEP 649's lazy annotation evaluation in
15
+ 3.14, a separate command annotated `secrets: list[str]` then resolved
16
+ `list` to the decorated `FunctionWithAio` object and failed with
17
+ `TypeError: 'FunctionWithAio' object is not subscriptable` at import
18
+ time, breaking every CLI entrypoint. The callbacks have been renamed
19
+ (`create_set`, `list_sets`, `list_agents`, `list_organizations`); the
20
+ user-facing command names (`pcc secrets set`, `pcc secrets list`,
21
+ `pcc agent list`, `pcc organizations list`) are unchanged.
22
+
23
+ ## [0.7.0] - 2026-05-07
24
+
25
+ ### Added
26
+
27
+ - `pcc secrets list` now shows a `Status` column for each secret set
28
+ (`ready`, `pending`, or `failed`). When a single set is requested
29
+ (`pcc secrets list <name>`), the readiness status is shown in the panel
30
+ title alongside the keys, and a follow-up message explains pending or
31
+ failed states. Backed by a new `secrets_get` API client method that
32
+ fetches a single secret set's full payload, including readiness fields.
33
+
34
+ ### Changed
35
+
36
+ - `pcc agent status` and the `pcc deploy` polling output now show
37
+ customer-friendly headlines for failed deployments when the API
38
+ supplies them. An OOM-killed container reads "Out of memory" instead
39
+ of "CrashLoopBackOff (OOMKilled, exit 137)"; an application crash
40
+ reads "Application Error" with the captured traceback on the next
41
+ line. Exit codes render as "exit code N" rather than "exit N" for
42
+ clarity. Older API responses without the new field continue to render
43
+ via the previous format.
44
+
8
45
  ## [0.6.0] - 2026-04-22
9
46
 
10
47
  ### 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
@@ -56,7 +56,6 @@ include = ["pipecatcloud*"]
56
56
  pipecatcloud = ["py.typed"]
57
57
 
58
58
  [tool.ruff]
59
- exclude = ["*_pb2.py"]
60
59
  line-length = 100
61
60
 
62
61
  [tool.ruff.lint]
@@ -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 reason:
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:
@@ -89,7 +89,7 @@ def format_cpu(millicores: int) -> str:
89
89
  @agent_cli.command(name="list", help="List agents in an organization.")
90
90
  @synchronizer.create_blocking
91
91
  @requires_login
92
- async def list(
92
+ async def list_agents(
93
93
  organization: str = typer.Option(
94
94
  None, "--organization", "-o", help="Organization to list agents for"
95
95
  ),
@@ -96,7 +96,7 @@ async def select(organization: str = typer.Option(None, "--organization", "-o"))
96
96
  @organization_cli.command(name="list", help="List organizations user is a member of.")
97
97
  @synchronizer.create_blocking
98
98
  @requires_login
99
- async def list():
99
+ async def list_organizations():
100
100
  current_org = config.get("org")
101
101
 
102
102
  with console.status(
@@ -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
 
@@ -65,7 +76,7 @@ def validate_secret_name(name: str):
65
76
  @secrets_cli.command(name="set", help="Create a new secret set for active organization")
66
77
  @synchronizer.create_blocking
67
78
  @requires_login
68
- async def set(
79
+ async def create_set(
69
80
  name: str = typer.Argument(help="Name of the secret set to create e.g. 'my-secret-set'"),
70
81
  secrets: list[str] = typer.Argument(
71
82
  None,
@@ -350,7 +361,7 @@ async def unset(
350
361
  @secrets_cli.command(name="list", help="List secret sets and set keys")
351
362
  @synchronizer.create_blocking
352
363
  @requires_login
353
- async def list(
364
+ async def list_sets(
354
365
  name: str = typer.Argument(
355
366
  None, help="Name of the secret set to list secrets from e.g. 'my-secret-set'"
356
367
  ),
@@ -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
- data, error = await API.bubble_error().secrets_list(org=org, secret_set=name, region=region)
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
- # Match
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 data:
424
+ for s in secrets:
409
425
  table.add_row(s["fieldName"])
410
- console.print(
411
- Panel(
412
- table,
413
- title=f"[bold]Secret keys for set [green]{name}[/green][/bold]",
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(secret_set["name"], secret_set["region"], set_type)
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(secret_set["name"], secret_set["region"])
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
@@ -1,6 +1,7 @@
1
1
  .gitignore
2
2
  .pre-commit-config.yaml
3
3
  CHANGELOG.md
4
+ CLAUDE.md
4
5
  LICENSE
5
6
  README.md
6
7
  pipecat-cloud.png
@@ -11,6 +12,7 @@ uv.lock
11
12
  .github/workflows/format.yml
12
13
  .github/workflows/publish-pypi.yml
13
14
  .github/workflows/publish-test.yml
15
+ .github/workflows/python-compatibility.yml
14
16
  .github/workflows/tests.yml
15
17
  scripts/fix-ruff.sh
16
18
  src/pipecatcloud/__init__.py
@@ -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:
@@ -179,7 +179,7 @@ class TestCLIRegionValidation:
179
179
  """Secrets set should accept valid regions from API."""
180
180
  # Arrange
181
181
  from pipecatcloud._utils import regions
182
- from pipecatcloud.cli.commands.secrets import set as secrets_set
182
+ from pipecatcloud.cli.commands.secrets import create_set as secrets_set
183
183
 
184
184
  regions._regions_cache = None
185
185
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes