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.
- pipecatcloud-0.7.0/.claude/settings.json +5 -0
- pipecatcloud-0.7.0/.pre-commit-config.yaml +13 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/CHANGELOG.md +47 -0
- pipecatcloud-0.7.0/CLAUDE.md +119 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/PKG-INFO +1 -1
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/pyproject.toml +13 -1
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/auth_utils.py +1 -2
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/build_utils.py +40 -14
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/console_utils.py +8 -9
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/deploy_utils.py +52 -31
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/regions.py +3 -5
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/agent.py +4 -4
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/api.py +64 -29
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/agent.py +20 -5
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/auth.py +10 -10
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/build.py +6 -8
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/deploy.py +29 -8
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/docker.py +2 -3
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/organizations.py +60 -17
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/secrets.py +48 -18
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/config.py +7 -8
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/exception.py +2 -3
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/session.py +5 -5
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/smallwebrtc/session_manager.py +2 -3
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/SOURCES.txt +4 -1
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_api_client.py +39 -5
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_auth.py +3 -5
- pipecatcloud-0.7.0/tests/test_build_utils.py +151 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_deployment_status.py +92 -12
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_krisp_viva.py +3 -4
- pipecatcloud-0.7.0/tests/test_max_session_duration.py +154 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_pat_auth.py +3 -4
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/uv.lock +3 -3
- pipecatcloud-0.5.0/.pre-commit-config.yaml +0 -8
- pipecatcloud-0.5.0/scripts/pre-commit.sh +0 -27
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/.gitignore +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/LICENSE +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/README.md +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/pytest.ini +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/setup.cfg +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/__init__.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/conftest.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_config_file.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-0.5.0 → pipecatcloud-0.7.0}/tests/test_regions.py +0 -0
|
@@ -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.
|
|
@@ -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) ->
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
182
|
+
with open(dockerignore_path) as f:
|
|
163
183
|
for line in f:
|
|
164
|
-
|
|
184
|
+
stripped = line.strip()
|
|
165
185
|
# Skip comments and empty lines
|
|
166
|
-
if
|
|
167
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
367
|
-
) ->
|
|
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:
|
|
22
|
-
title_extra:
|
|
23
|
-
subtitle:
|
|
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:
|
|
43
|
-
title_extra:
|
|
44
|
-
subtitle:
|
|
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:
|
|
78
|
-
title:
|
|
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
|
|
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:
|
|
46
|
-
current_revision:
|
|
47
|
-
previous_revision:
|
|
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) ->
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
257
|
-
max_agents:
|
|
267
|
+
min_agents: int | None = 0
|
|
268
|
+
max_agents: int | None = None
|
|
258
269
|
# @deprecated
|
|
259
|
-
min_instances:
|
|
270
|
+
min_instances: int | None = field(default=None, metadata={"deprecated": True})
|
|
260
271
|
# @deprecated
|
|
261
|
-
max_instances:
|
|
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:
|
|
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:
|
|
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:
|
|
325
|
-
image:
|
|
326
|
-
build_id:
|
|
327
|
-
image_credentials:
|
|
328
|
-
secret_set:
|
|
329
|
-
region:
|
|
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:
|
|
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() ->
|
|
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
|
|
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:
|
|
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
|
-
|
|
463
|
-
|
|
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:
|
|
17
|
+
_regions_cache: list[dict[str, str]] | None = None
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
async def get_regions() ->
|
|
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() ->
|
|
46
|
+
async def get_region_codes() -> list[str]:
|
|
49
47
|
"""
|
|
50
48
|
Get list of region codes only.
|
|
51
49
|
|