pipecatcloud 0.5.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.
Files changed (71) hide show
  1. pipecatcloud-0.7.0/.claude/settings.json +5 -0
  2. pipecatcloud-0.7.0/.pre-commit-config.yaml +13 -0
  3. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/CHANGELOG.md +47 -0
  4. pipecatcloud-0.7.0/CLAUDE.md +119 -0
  5. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/PKG-INFO +1 -1
  6. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/pyproject.toml +13 -1
  7. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/auth_utils.py +1 -2
  8. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/build_utils.py +40 -14
  9. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/console_utils.py +8 -9
  10. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/deploy_utils.py +52 -31
  11. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/regions.py +3 -5
  12. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/agent.py +4 -4
  13. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/api.py +64 -29
  14. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/agent.py +20 -5
  15. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/auth.py +10 -10
  16. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/build.py +6 -8
  17. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/deploy.py +29 -8
  18. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/docker.py +2 -3
  19. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/organizations.py +60 -17
  20. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/secrets.py +48 -18
  21. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/config.py +7 -8
  22. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/exception.py +2 -3
  23. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/session.py +5 -5
  24. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/smallwebrtc/session_manager.py +2 -3
  25. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
  26. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/SOURCES.txt +4 -1
  27. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_api_client.py +39 -5
  28. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_auth.py +3 -5
  29. pipecatcloud-0.7.0/tests/test_build_utils.py +151 -0
  30. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_deployment_status.py +92 -12
  31. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_krisp_viva.py +3 -4
  32. pipecatcloud-0.7.0/tests/test_max_session_duration.py +154 -0
  33. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_pat_auth.py +3 -4
  34. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/uv.lock +3 -3
  35. pipecatcloud-0.5.0/.pre-commit-config.yaml +0 -8
  36. pipecatcloud-0.5.0/scripts/pre-commit.sh +0 -27
  37. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/format.yml +0 -0
  38. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/publish-pypi.yml +0 -0
  39. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/publish-test.yml +0 -0
  40. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/tests.yml +0 -0
  41. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.gitignore +0 -0
  42. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/LICENSE +0 -0
  43. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/README.md +0 -0
  44. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/pipecat-cloud.png +0 -0
  45. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/pytest.ini +0 -0
  46. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/scripts/fix-ruff.sh +0 -0
  47. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/setup.cfg +0 -0
  48. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__init__.py +0 -0
  49. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__main__.py +0 -0
  50. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__version__.py +0 -0
  51. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/__init__.py +0 -0
  52. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
  53. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/__init__.py +0 -0
  54. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/api.py +0 -0
  55. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  56. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
  57. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/entry_point.py +0 -0
  58. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/config.py +0 -0
  59. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/constants.py +0 -0
  60. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/py.typed +0 -0
  61. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  62. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  63. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
  64. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  65. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/__init__.py +0 -0
  66. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/conftest.py +0 -0
  67. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_agent_sessions.py +0 -0
  68. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_agent_stop.py +0 -0
  69. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_config_file.py +0 -0
  70. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_docker_commands.py +0 -0
  71. {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_regions.py +0 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "attribution": {
3
+ "commit": ""
4
+ }
5
+ }
@@ -0,0 +1,13 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff
5
+ name: ruff
6
+ entry: uv run ruff check --fix
7
+ language: system
8
+ types: [python]
9
+ - id: ruff-format
10
+ name: ruff-format
11
+ entry: uv run ruff format
12
+ language: system
13
+ types: [python]
@@ -5,6 +5,53 @@ 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
+
30
+ ## [0.6.0] - 2026-04-22
31
+
32
+ ### Added
33
+
34
+ - `websocket_auth` config option in `pcc-deploy.toml` to control authentication
35
+ mode for the generic WebSocket handler (`/ws/generic`). Valid values are
36
+ `"none"` (no authentication required) and `"token"` (requires a session token
37
+ obtained via `/start`). New services default to `"token"` if not specified.
38
+
39
+ - `--max-session-duration` flag on `pcc deploy` for configuring the
40
+ per-service maximum session duration in seconds. Accepts values
41
+ between 60 and 14400 (4 hours), defaulting to 7200 (2 hours) when
42
+ unset. Also exposed in `pcc agent status` output and as
43
+ `max_session_duration` in `pcc-deploy.toml`.
44
+
45
+ ### Fixed
46
+
47
+ - `.dockerignore` patterns written with Docker's conventional trailing
48
+ slash (e.g. `.venv/`, `node_modules/`) and leading `./` anchors
49
+ (e.g. `./dist`) now match correctly in cloud builds. Previously the
50
+ CLI compared patterns with `fnmatch`, which treats both forms as
51
+ literal text, so these entries silently excluded nothing and large
52
+ directories like `.venv/` could leak into the build context and
53
+ trip the 500 MB limit.
54
+
8
55
  ## [0.5.0] - 2026-04-17
9
56
 
10
57
  ### Changed
@@ -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.5.0
3
+ Version: 0.7.0
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,9 +56,21 @@ include = ["pipecatcloud*"]
56
56
  pipecatcloud = ["py.typed"]
57
57
 
58
58
  [tool.ruff]
59
- exclude = ["*_pb2.py"]
60
59
  line-length = 100
61
60
 
61
+ [tool.ruff.lint]
62
+ select = [
63
+ "I", # Import rules
64
+ "UP", # Modern Python
65
+ ]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ # Skip docstring checks for non-source code
69
+ "tests/**/*.py" = ["D"]
70
+ "scripts/**/*.py" = ["D"]
71
+ # Skip D104 (missing docstring in public package) for __init__.py files
72
+ "**/__init__.py" = ["D104"]
73
+
62
74
  [project.urls]
63
75
  Website = "https://docs.pipecat.daily.co/"
64
76
 
@@ -5,7 +5,6 @@
5
5
  #
6
6
 
7
7
  import functools
8
- from typing import Optional
9
8
 
10
9
  import aiohttp
11
10
 
@@ -15,7 +14,7 @@ from pipecatcloud.cli import PIPECAT_CLI_NAME
15
14
  from pipecatcloud.cli.config import config
16
15
 
17
16
 
18
- async def _resolve_default_org(token: str) -> Optional[str]:
17
+ async def _resolve_default_org(token: str) -> str | None:
19
18
  """Fetch the user's default organization using the given token."""
20
19
  from pipecatcloud.config import config as base_config
21
20
 
@@ -19,15 +19,15 @@ import io
19
19
  import os
20
20
  import tarfile
21
21
  import time
22
+ from collections.abc import Callable
22
23
  from dataclasses import dataclass
23
24
  from pathlib import Path
24
- from typing import Callable, List, Optional, Set, Tuple
25
25
 
26
26
  import aiohttp
27
27
  from loguru import logger
28
28
 
29
29
  # Default patterns to exclude from build context
30
- DEFAULT_EXCLUSIONS: Set[str] = {
30
+ DEFAULT_EXCLUSIONS: set[str] = {
31
31
  # Version control
32
32
  ".git",
33
33
  ".gitignore",
@@ -116,7 +116,7 @@ class BuildStatus:
116
116
  return status in cls.TERMINAL_STATUSES
117
117
 
118
118
 
119
- def _should_exclude(path: Path, exclusions: Set[str], base_path: Path) -> bool:
119
+ def _should_exclude(path: Path, exclusions: set[str], base_path: Path) -> bool:
120
120
  """
121
121
  Check if a path should be excluded from the build context.
122
122
 
@@ -143,7 +143,27 @@ def _should_exclude(path: Path, exclusions: Set[str], base_path: Path) -> bool:
143
143
  return False
144
144
 
145
145
 
146
- def load_dockerignore(context_dir: Path) -> Optional[Set[str]]:
146
+ def _normalize_pattern(pattern: str) -> str:
147
+ """Normalize a single ``.dockerignore`` pattern for ``fnmatch`` matching.
148
+
149
+ The matcher in ``_should_exclude`` walks the tree and compares each
150
+ path component against every pattern with ``fnmatch.fnmatch``, which
151
+ treats the pattern as literal text. ``.dockerignore`` (and the
152
+ companion ``.gitignore`` grammar users pattern after) allows a few
153
+ pieces of sugar that ``fnmatch`` does not understand: a trailing
154
+ slash meaning "directory only", and a leading ``./`` anchoring to
155
+ the context root. Both forms are common in the wild and routinely
156
+ silently match nothing under a raw ``fnmatch`` implementation, so
157
+ we strip them here to make the pattern intent actually take effect.
158
+ """
159
+ normalized = pattern.strip()
160
+ if normalized.startswith("./"):
161
+ normalized = normalized[2:]
162
+ normalized = normalized.rstrip("/")
163
+ return normalized
164
+
165
+
166
+ def load_dockerignore(context_dir: Path) -> set[str] | None:
147
167
  """
148
168
  Load patterns from .dockerignore file if it exists.
149
169
 
@@ -159,12 +179,15 @@ def load_dockerignore(context_dir: Path) -> Optional[Set[str]]:
159
179
 
160
180
  patterns = set()
161
181
  try:
162
- with open(dockerignore_path, "r") as f:
182
+ with open(dockerignore_path) as f:
163
183
  for line in f:
164
- line = line.strip()
184
+ stripped = line.strip()
165
185
  # Skip comments and empty lines
166
- if line and not line.startswith("#"):
167
- patterns.add(line)
186
+ if not stripped or stripped.startswith("#"):
187
+ continue
188
+ normalized = _normalize_pattern(stripped)
189
+ if normalized:
190
+ patterns.add(normalized)
168
191
  except Exception as e:
169
192
  logger.warning(f"Failed to read .dockerignore: {e}")
170
193
  return None
@@ -172,7 +195,7 @@ def load_dockerignore(context_dir: Path) -> Optional[Set[str]]:
172
195
  return patterns
173
196
 
174
197
 
175
- def get_exclusions(context_dir: Path, extra_patterns: Optional[List[str]] = None) -> Set[str]:
198
+ def get_exclusions(context_dir: Path, extra_patterns: list[str] | None = None) -> set[str]:
176
199
  """
177
200
  Get the set of exclusion patterns to use.
178
201
 
@@ -195,13 +218,16 @@ def get_exclusions(context_dir: Path, extra_patterns: Optional[List[str]] = None
195
218
  # Use defaults + extras
196
219
  exclusions = DEFAULT_EXCLUSIONS.copy()
197
220
  if extra_patterns:
198
- exclusions.update(extra_patterns)
221
+ for extra in extra_patterns:
222
+ normalized = _normalize_pattern(extra)
223
+ if normalized:
224
+ exclusions.add(normalized)
199
225
  return exclusions
200
226
 
201
227
 
202
228
  def create_deterministic_tarball(
203
229
  context_dir: str,
204
- exclusions: Set[str],
230
+ exclusions: set[str],
205
231
  dockerfile_path: str = "Dockerfile",
206
232
  ) -> BuildContext:
207
233
  """
@@ -235,7 +261,7 @@ def create_deterministic_tarball(
235
261
  raise FileNotFoundError(f"Dockerfile not found: {dockerfile_full}")
236
262
 
237
263
  # Collect all files, sorted alphabetically
238
- files_to_add: List[Tuple[Path, str]] = []
264
+ files_to_add: list[tuple[Path, str]] = []
239
265
 
240
266
  for root, dirs, files in os.walk(base_path):
241
267
  root_path = Path(root)
@@ -363,8 +389,8 @@ async def poll_build_status(
363
389
  api_client,
364
390
  poll_interval: float = 3.0,
365
391
  max_duration: float = 600.0, # 10 minutes
366
- status_callback: Optional[Callable[[dict], None]] = None,
367
- ) -> Tuple[bool, dict]:
392
+ status_callback: Callable[[dict], None] | None = None,
393
+ ) -> tuple[bool, dict]:
368
394
  """
369
395
  Poll build status until completion or timeout.
370
396
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  import statistics
8
8
  from datetime import datetime
9
- from typing import Optional, Union
10
9
 
11
10
  from rich.console import Console
12
11
  from rich.panel import Panel
@@ -18,9 +17,9 @@ class PipecatConsole(Console):
18
17
  def success(
19
18
  self,
20
19
  message,
21
- title: Optional[str] = None,
22
- title_extra: Optional[str] = None,
23
- subtitle: Optional[str] = None,
20
+ title: str | None = None,
21
+ title_extra: str | None = None,
22
+ subtitle: str | None = None,
24
23
  ):
25
24
  if not title:
26
25
  title = f"{PANEL_TITLE_SUCCESS}{f' - {title_extra}' if title_extra is not None else ''}"
@@ -39,9 +38,9 @@ class PipecatConsole(Console):
39
38
  def error(
40
39
  self,
41
40
  message,
42
- title: Optional[str] = None,
43
- title_extra: Optional[str] = None,
44
- subtitle: Optional[str] = None,
41
+ title: str | None = None,
42
+ title_extra: str | None = None,
43
+ subtitle: str | None = None,
45
44
  ):
46
45
  if not title:
47
46
  title = f"{PANEL_TITLE_ERROR}{f' - {title_extra}' if title_extra is not None else ''}"
@@ -74,8 +73,8 @@ class PipecatConsole(Console):
74
73
 
75
74
  def api_error(
76
75
  self,
77
- error_code: Optional[Union[str, dict]] = None,
78
- title: Optional[str] = "API Error",
76
+ error_code: str | dict | None = None,
77
+ title: str | None = "API Error",
79
78
  hide_subtitle: bool = False,
80
79
  ):
81
80
  DEFAULT_ERROR_MESSAGE = "Unknown error. Please contact support."
@@ -6,10 +6,9 @@
6
6
 
7
7
  import functools
8
8
  import os
9
- from datetime import datetime, timezone
9
+ from collections.abc import Callable
10
+ from datetime import UTC, datetime
10
11
  from enum import Enum
11
- from typing import Callable, List, Optional
12
-
13
12
 
14
13
  import toml
15
14
  import typer
@@ -42,12 +41,12 @@ class DeploymentStatus:
42
41
  status_message: str
43
42
  is_available: bool = False
44
43
  is_ready: bool = False
45
- degraded_reason: Optional[str] = None
46
- current_revision: Optional[dict] = None
47
- previous_revision: Optional[dict] = None
44
+ degraded_reason: str | None = None
45
+ current_revision: dict | None = None
46
+ previous_revision: dict | None = None
48
47
 
49
48
 
50
- def _find_condition(conditions: list, condition_type: str) -> Optional[dict]:
49
+ def _find_condition(conditions: list, condition_type: str) -> dict | None:
51
50
  """Find a condition by type from the conditions array."""
52
51
  for c in conditions:
53
52
  if c.get("type") == condition_type:
@@ -55,13 +54,13 @@ def _find_condition(conditions: list, condition_type: str) -> Optional[dict]:
55
54
  return None
56
55
 
57
56
 
58
- def _format_elapsed(phase_started_at: Optional[str]) -> str:
57
+ def _format_elapsed(phase_started_at: str | None) -> str:
59
58
  """Format elapsed time since phaseStartedAt as a human-readable string."""
60
59
  if not phase_started_at:
61
60
  return ""
62
61
  try:
63
62
  started = datetime.fromisoformat(phase_started_at.replace("Z", "+00:00"))
64
- elapsed = datetime.now(timezone.utc) - started
63
+ elapsed = datetime.now(UTC) - started
65
64
  total_seconds = int(elapsed.total_seconds())
66
65
  if total_seconds < 0:
67
66
  return ""
@@ -105,19 +104,26 @@ def format_health_lines(health: dict) -> list:
105
104
  """Format health details as indented lines under a revision."""
106
105
  lines = []
107
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")
108
112
  reason = health.get("reason", "")
109
113
  term_reason = health.get("lastTerminationReason", "")
110
114
  exit_code = health.get("lastExitCode")
111
115
 
112
- if reason:
116
+ if headline:
117
+ reason_parts.append(f"[red]{headline}[/red]")
118
+ elif reason:
113
119
  detail = reason
114
120
  if term_reason and term_reason != reason:
115
121
  detail += f" ({term_reason}"
116
122
  if exit_code is not None:
117
- detail += f", exit {exit_code}"
123
+ detail += f", exit code {exit_code}"
118
124
  detail += ")"
119
125
  elif exit_code is not None:
120
- detail += f" (exit {exit_code})"
126
+ detail += f" (exit code {exit_code})"
121
127
  reason_parts.append(f"[red]{detail}[/red]")
122
128
 
123
129
  restarts = health.get("restartCount", 0)
@@ -125,6 +131,11 @@ def format_health_lines(health: dict) -> list:
125
131
  if restarts > 0:
126
132
  reason_parts.append(f"[dim]·[/dim] {restarts} restarts across {replicas_started} replicas")
127
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
+
128
139
  if reason_parts:
129
140
  lines.append(" " + " ".join(reason_parts))
130
141
 
@@ -149,7 +160,7 @@ def _build_status_message(headline: str, current_rev=None, previous_rev=None) ->
149
160
 
150
161
  def interpret_deployment_status(
151
162
  agent_status: dict,
152
- desired_deployment_id: Optional[str] = None,
163
+ desired_deployment_id: str | None = None,
153
164
  ) -> DeploymentStatus:
154
165
  """Interpret the raw API response into a structured deployment status.
155
166
 
@@ -253,12 +264,12 @@ def interpret_deployment_status(
253
264
 
254
265
  @dataclass
255
266
  class ScalingParams:
256
- min_agents: Optional[int] = 0
257
- max_agents: Optional[int] = None
267
+ min_agents: int | None = 0
268
+ max_agents: int | None = None
258
269
  # @deprecated
259
- min_instances: Optional[int] = field(default=None, metadata={"deprecated": True})
270
+ min_instances: int | None = field(default=None, metadata={"deprecated": True})
260
271
  # @deprecated
261
- max_instances: Optional[int] = field(default=None, metadata={"deprecated": True})
272
+ max_instances: int | None = field(default=None, metadata={"deprecated": True})
262
273
 
263
274
  def __attrs_post_init__(self):
264
275
  # Handle deprecated fields
@@ -288,7 +299,7 @@ class ScalingParams:
288
299
 
289
300
  @dataclass
290
301
  class KrispVivaConfig:
291
- audio_filter: Optional[KrispVivaAudioFilter] = None
302
+ audio_filter: KrispVivaAudioFilter | None = None
292
303
 
293
304
  def __attrs_post_init__(self):
294
305
  # Validation against known models
@@ -309,7 +320,7 @@ class BuildConfig:
309
320
 
310
321
  context_dir: str = "."
311
322
  dockerfile: str = "Dockerfile"
312
- exclude_patterns: List[str] = field(factory=list)
323
+ exclude_patterns: list[str] = field(factory=list)
313
324
 
314
325
  def to_dict(self):
315
326
  return {
@@ -321,19 +332,21 @@ class BuildConfig:
321
332
 
322
333
  @dataclass
323
334
  class DeployConfigParams:
324
- agent_name: Optional[str] = None
325
- image: Optional[str] = None
326
- build_id: Optional[str] = None # For cloud builds
327
- image_credentials: Optional[str] = None
328
- secret_set: Optional[str] = None
329
- region: Optional[str] = None
335
+ agent_name: str | None = None
336
+ image: str | None = None
337
+ build_id: str | None = None # For cloud builds
338
+ image_credentials: str | None = None
339
+ secret_set: str | None = None
340
+ region: str | None = None
330
341
  scaling: ScalingParams = ScalingParams()
331
342
  enable_krisp: bool = False
332
343
  docker_config: dict = field(factory=dict)
333
344
  build_config: BuildConfig = field(factory=BuildConfig) # Cloud build configuration
334
- agent_profile: Optional[str] = None
345
+ agent_profile: str | None = None
335
346
  krisp_viva: KrispVivaConfig = field(factory=KrispVivaConfig)
336
347
  force_redeploy: bool = False
348
+ websocket_auth: str | None = None
349
+ max_session_duration: int | None = None
337
350
 
338
351
  def __attrs_post_init__(self):
339
352
  if self.image is not None and ":" not in self.image:
@@ -341,6 +354,8 @@ class DeployConfigParams:
341
354
  # Cannot specify both image and build_id
342
355
  if self.image is not None and self.build_id is not None:
343
356
  raise ValueError("Cannot specify both 'image' and 'build_id'")
357
+ if self.max_session_duration is not None and not 60 <= self.max_session_duration <= 14400:
358
+ raise ValueError("max_session_duration must be between 60 and 14400 seconds")
344
359
 
345
360
  def to_dict(self):
346
361
  return {
@@ -356,17 +371,19 @@ class DeployConfigParams:
356
371
  "build_config": self.build_config.to_dict() if self.build_config else None,
357
372
  "agent_profile": self.agent_profile,
358
373
  "krisp_viva": self.krisp_viva.to_dict() if self.krisp_viva else None,
374
+ "websocket_auth": self.websocket_auth,
375
+ "max_session_duration": self.max_session_duration,
359
376
  }
360
377
 
361
378
 
362
- def load_deploy_config_file() -> Optional[DeployConfigParams]:
379
+ def load_deploy_config_file() -> DeployConfigParams | None:
363
380
  from pipecatcloud.cli.config import deploy_config_path
364
381
 
365
382
  logger.debug(f"Deploy config path: {deploy_config_path}")
366
383
  logger.debug(f"Deploy config path exists: {os.path.exists(deploy_config_path)}")
367
384
 
368
385
  try:
369
- with open(deploy_config_path, "r") as f:
386
+ with open(deploy_config_path) as f:
370
387
  config_data = toml.load(f)
371
388
  except Exception:
372
389
  return None
@@ -415,6 +432,8 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
415
432
  "build",
416
433
  "agent_profile",
417
434
  "krisp_viva",
435
+ "websocket_auth",
436
+ "max_session_duration",
418
437
  }
419
438
  unexpected_keys = set(config_data.keys()) - expected_keys
420
439
  if unexpected_keys:
@@ -427,7 +446,7 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
427
446
  raise ConfigFileError(str(e))
428
447
 
429
448
 
430
- CONFIG_FILE_OPTION: Optional[str] = typer.Option(
449
+ CONFIG_FILE_OPTION: str | None = typer.Option(
431
450
  None,
432
451
  "--config-file",
433
452
  help=f"Path to deploy config file (default: {PIPECAT_DEPLOY_CONFIG_PATH})",
@@ -459,8 +478,10 @@ def with_deploy_config(func: Callable) -> Callable:
459
478
  deploy_config = load_deploy_config_file()
460
479
  kwargs["deploy_config"] = deploy_config
461
480
  except Exception as e:
462
- logger.error(f"Error loading deploy config: {e}")
463
- raise ConfigFileError(str(e))
481
+ from pipecatcloud._utils.console_utils import console
482
+
483
+ console.error(f"Error loading deploy config: {e}")
484
+ raise typer.Exit(1)
464
485
  return func(*args, **kwargs)
465
486
 
466
487
  return wrapper
@@ -10,16 +10,14 @@ Region utilities for Pipecat Cloud CLI.
10
10
  Provides functions to fetch and cache available regions from the API.
11
11
  """
12
12
 
13
- from typing import Dict, List, Optional
14
-
15
13
  from pipecatcloud.cli.api import API
16
14
  from pipecatcloud.cli.config import config
17
15
 
18
16
  # Module-level cache for regions
19
- _regions_cache: Optional[List[Dict[str, str]]] = None
17
+ _regions_cache: list[dict[str, str]] | None = None
20
18
 
21
19
 
22
- async def get_regions() -> List[Dict[str, str]]:
20
+ async def get_regions() -> list[dict[str, str]]:
23
21
  """
24
22
  Fetch available regions from the API with caching.
25
23
 
@@ -45,7 +43,7 @@ async def get_regions() -> List[Dict[str, str]]:
45
43
  return _regions_cache
46
44
 
47
45
 
48
- async def get_region_codes() -> List[str]:
46
+ async def get_region_codes() -> list[str]:
49
47
  """
50
48
  Get list of region codes only.
51
49