pipecatcloud 0.4.1__tar.gz → 0.4.2__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.4.1 → pipecatcloud-0.4.2}/CHANGELOG.md +24 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/PKG-INFO +1 -1
- pipecatcloud-0.4.2/src/pipecatcloud/_utils/auth_utils.py +67 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/deploy_utils.py +48 -2
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/api.py +8 -3
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/agent.py +18 -1
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/auth.py +3 -2
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_deployment_status.py +154 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_pat_auth.py +5 -3
- pipecatcloud-0.4.1/src/pipecatcloud/_utils/auth_utils.py +0 -26
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/.github/workflows/format.yml +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/.gitignore +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/LICENSE +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/README.md +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/pyproject.toml +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/pytest.ini +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/scripts/pre-commit.sh +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/setup.cfg +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/build_utils.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/regions.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/build.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/deploy.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/docker.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/secrets.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/SOURCES.txt +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/__init__.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/conftest.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_api_client.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_config_file.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/tests/test_regions.py +0 -0
- {pipecatcloud-0.4.1 → pipecatcloud-0.4.2}/uv.lock +0 -0
|
@@ -5,6 +5,30 @@ 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.4.2] - 2026-04-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Container health diagnostics in `agent status` and `deploy`**: When a
|
|
13
|
+
deployment is failing, the CLI now shows the failure reason (e.g.,
|
|
14
|
+
CrashLoopBackOff, OOMKilled, ImagePullBackOff), exit code, restart count,
|
|
15
|
+
and the actual error output (e.g., Python tracebacks). This information
|
|
16
|
+
appears under the revision line in both `pcc agent status` and during
|
|
17
|
+
deploy monitoring.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- All API requests now include a `User-Agent: PipecatCloudCLI/<version>` header,
|
|
22
|
+
enabling server-side identification of CLI versions for debugging.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Default organization resolution for `PIPECAT_TOKEN` auth**: The CLI now
|
|
27
|
+
automatically resolves the user's default organization from the API when
|
|
28
|
+
a token is present but no org is configured locally. Previously,
|
|
29
|
+
`PIPECAT_TOKEN` required the org to be in the config file or `PIPECAT_ORG`
|
|
30
|
+
to be set as well.
|
|
31
|
+
|
|
8
32
|
## [0.4.1] - 2026-03-30
|
|
9
33
|
|
|
10
34
|
### Added
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from pipecatcloud.__version__ import version
|
|
13
|
+
from pipecatcloud._utils.console_utils import console
|
|
14
|
+
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
15
|
+
from pipecatcloud.cli.config import config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def _resolve_default_org(token: str) -> Optional[str]:
|
|
19
|
+
"""Fetch the user's default organization using the given token."""
|
|
20
|
+
from pipecatcloud.config import config as base_config
|
|
21
|
+
|
|
22
|
+
api_host = base_config.get("api_host")
|
|
23
|
+
org_path = base_config.get("organization_path")
|
|
24
|
+
url = f"{api_host}{org_path}"
|
|
25
|
+
|
|
26
|
+
async with aiohttp.ClientSession() as session:
|
|
27
|
+
async with session.get(
|
|
28
|
+
url,
|
|
29
|
+
headers={
|
|
30
|
+
"Authorization": f"Bearer {token}",
|
|
31
|
+
"User-Agent": f"PipecatCloudCLI/{version}",
|
|
32
|
+
},
|
|
33
|
+
) as resp:
|
|
34
|
+
if resp.status == 200:
|
|
35
|
+
data = await resp.json()
|
|
36
|
+
organizations = data.get("organizations", [])
|
|
37
|
+
if organizations:
|
|
38
|
+
return organizations[0]["name"]
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def requires_login(func):
|
|
43
|
+
@functools.wraps(func)
|
|
44
|
+
async def wrapper(*args, **kwargs):
|
|
45
|
+
token = config.get("token")
|
|
46
|
+
if token is None:
|
|
47
|
+
console.error(
|
|
48
|
+
f"You are not logged in. Please run `{PIPECAT_CLI_NAME} auth login` first.",
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# When org is not set locally (e.g. PIPECAT_TOKEN without a config file),
|
|
53
|
+
# resolve the user's default organization from the API.
|
|
54
|
+
if config.get("org") is None:
|
|
55
|
+
org_name = await _resolve_default_org(token)
|
|
56
|
+
if org_name:
|
|
57
|
+
config.override_locally("org", org_name)
|
|
58
|
+
else:
|
|
59
|
+
console.error(
|
|
60
|
+
"Could not determine your organization. "
|
|
61
|
+
f"Set PIPECAT_ORG or run `{PIPECAT_CLI_NAME} auth login`.",
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
return await func(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
return wrapper
|
|
@@ -75,7 +75,7 @@ def _format_elapsed(phase_started_at: Optional[str]) -> str:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def _format_revision_line(label: str, rev: dict) -> str:
|
|
78
|
-
"""Format a single revision as an indented detail line."""
|
|
78
|
+
"""Format a single revision as an indented detail line, with health details if present."""
|
|
79
79
|
deploy_id = rev.get("deploymentID", "unknown")[:8]
|
|
80
80
|
phase = rev.get("phase", "Unknown")
|
|
81
81
|
ready_replicas = rev.get("readyReplicas")
|
|
@@ -88,7 +88,53 @@ def _format_revision_line(label: str, rev: dict) -> str:
|
|
|
88
88
|
if elapsed:
|
|
89
89
|
parts.append(f"[dim]·[/dim] [dim]{elapsed}[/dim]")
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
lines = [" ".join(parts)]
|
|
92
|
+
|
|
93
|
+
# Append health details if present and unhealthy
|
|
94
|
+
health = rev.get("health")
|
|
95
|
+
if health and not (health.get("ready") and health.get("restartCount", 0) == 0):
|
|
96
|
+
lines.extend(format_health_lines(health))
|
|
97
|
+
|
|
98
|
+
if rev.get("hasInfrastructureIssue"):
|
|
99
|
+
lines.append(" [yellow]Infrastructure issue detected — contact support[/yellow]")
|
|
100
|
+
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def format_health_lines(health: dict) -> list:
|
|
105
|
+
"""Format health details as indented lines under a revision."""
|
|
106
|
+
lines = []
|
|
107
|
+
reason_parts = []
|
|
108
|
+
reason = health.get("reason", "")
|
|
109
|
+
term_reason = health.get("lastTerminationReason", "")
|
|
110
|
+
exit_code = health.get("lastExitCode")
|
|
111
|
+
|
|
112
|
+
if reason:
|
|
113
|
+
detail = reason
|
|
114
|
+
if term_reason and term_reason != reason:
|
|
115
|
+
detail += f" ({term_reason}"
|
|
116
|
+
if exit_code is not None:
|
|
117
|
+
detail += f", exit {exit_code}"
|
|
118
|
+
detail += ")"
|
|
119
|
+
elif exit_code is not None:
|
|
120
|
+
detail += f" (exit {exit_code})"
|
|
121
|
+
reason_parts.append(f"[red]{detail}[/red]")
|
|
122
|
+
|
|
123
|
+
restarts = health.get("restartCount", 0)
|
|
124
|
+
replicas_started = health.get("replicasStarted", 0)
|
|
125
|
+
if restarts > 0:
|
|
126
|
+
reason_parts.append(f"[dim]·[/dim] {restarts} restarts across {replicas_started} replicas")
|
|
127
|
+
|
|
128
|
+
if reason_parts:
|
|
129
|
+
lines.append(" " + " ".join(reason_parts))
|
|
130
|
+
|
|
131
|
+
message = health.get("message", "")
|
|
132
|
+
if message:
|
|
133
|
+
last_line = message.strip().split("\n")[-1].strip()
|
|
134
|
+
if last_line:
|
|
135
|
+
lines.append(f" [dim]{last_line}[/dim]")
|
|
136
|
+
|
|
137
|
+
return lines
|
|
92
138
|
|
|
93
139
|
|
|
94
140
|
def _build_status_message(headline: str, current_rev=None, previous_rev=None) -> str:
|
|
@@ -12,6 +12,7 @@ from typing import Callable, List, Optional, Union
|
|
|
12
12
|
import aiohttp
|
|
13
13
|
from loguru import logger
|
|
14
14
|
|
|
15
|
+
from pipecatcloud.__version__ import version
|
|
15
16
|
from pipecatcloud._utils.deploy_utils import DeployConfigParams
|
|
16
17
|
from pipecatcloud.config import config
|
|
17
18
|
from pipecatcloud.exception import AgentStartError
|
|
@@ -49,9 +50,10 @@ class _API:
|
|
|
49
50
|
return f"{config.get('api_host', '')}{config.get(path, '')}"
|
|
50
51
|
|
|
51
52
|
def _configure_headers(self, override_token: Optional[str] = None) -> dict:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
headers = {"User-Agent": f"PipecatCloudCLI/{version}"}
|
|
54
|
+
if self.token or override_token:
|
|
55
|
+
headers["Authorization"] = f"Bearer {override_token or self.token}"
|
|
56
|
+
return headers
|
|
55
57
|
|
|
56
58
|
def _is_pat(self) -> bool:
|
|
57
59
|
"""Check if the current token is a Personal Access Token."""
|
|
@@ -109,6 +111,9 @@ class _API:
|
|
|
109
111
|
self.token = new_token
|
|
110
112
|
return new_token
|
|
111
113
|
|
|
114
|
+
# TODO: several auth.py and agent.py calls bypass _base_request() with their
|
|
115
|
+
# own aiohttp.ClientSession(). A longer-term cleanup could route all PCC API
|
|
116
|
+
# calls through _base_request() so headers (User-Agent, Auth) are set in one place.
|
|
112
117
|
async def _base_request(
|
|
113
118
|
self,
|
|
114
119
|
method: str,
|
|
@@ -20,6 +20,7 @@ from rich.panel import Panel
|
|
|
20
20
|
from rich.table import Table
|
|
21
21
|
from rich.text import Text
|
|
22
22
|
|
|
23
|
+
from pipecatcloud.__version__ import version as _cli_version
|
|
23
24
|
from pipecatcloud._utils.async_utils import synchronizer
|
|
24
25
|
from pipecatcloud._utils.auth_utils import requires_login
|
|
25
26
|
from pipecatcloud._utils.console_utils import (
|
|
@@ -31,6 +32,7 @@ from pipecatcloud._utils.console_utils import (
|
|
|
31
32
|
from pipecatcloud._utils.deploy_utils import (
|
|
32
33
|
CONFIG_FILE_OPTION,
|
|
33
34
|
DeployConfigParams,
|
|
35
|
+
format_health_lines,
|
|
34
36
|
with_deploy_config,
|
|
35
37
|
)
|
|
36
38
|
from pipecatcloud._utils.regions import get_region_codes, validate_region
|
|
@@ -285,6 +287,18 @@ async def status(
|
|
|
285
287
|
current_parts.append(f"[dim]·[/dim] {rev_replicas} agents")
|
|
286
288
|
health_lines.append(" ".join(current_parts))
|
|
287
289
|
|
|
290
|
+
# Show health details when available (skip if healthy with no restarts)
|
|
291
|
+
rev_health = current_rev.get("health")
|
|
292
|
+
if rev_health and not (
|
|
293
|
+
rev_health.get("ready") and rev_health.get("restartCount", 0) == 0
|
|
294
|
+
):
|
|
295
|
+
health_lines.extend(format_health_lines(rev_health))
|
|
296
|
+
|
|
297
|
+
if current_rev.get("hasInfrastructureIssue"):
|
|
298
|
+
health_lines.append(
|
|
299
|
+
" [yellow]Infrastructure issue detected — contact support[/yellow]"
|
|
300
|
+
)
|
|
301
|
+
|
|
288
302
|
if previous_rev:
|
|
289
303
|
prev_phase = previous_rev.get("phase", "Unknown")
|
|
290
304
|
prev_id = previous_rev.get("deploymentID", "")[:8]
|
|
@@ -720,7 +734,10 @@ async def deployments(
|
|
|
720
734
|
async with aiohttp.ClientSession() as session:
|
|
721
735
|
response = await session.get(
|
|
722
736
|
f"{API.construct_api_url('services_deployments_path').format(org=org, service=agent_name)}",
|
|
723
|
-
headers={
|
|
737
|
+
headers={
|
|
738
|
+
"Authorization": f"Bearer {token}",
|
|
739
|
+
"User-Agent": f"PipecatCloudCLI/{_cli_version}",
|
|
740
|
+
},
|
|
724
741
|
)
|
|
725
742
|
if response.status != 200:
|
|
726
743
|
error_code = str(response.status)
|
|
@@ -20,6 +20,7 @@ from loguru import logger
|
|
|
20
20
|
from rich.columns import Columns
|
|
21
21
|
from rich.live import Live
|
|
22
22
|
|
|
23
|
+
from pipecatcloud.__version__ import version as _cli_version
|
|
23
24
|
from pipecatcloud._utils.async_utils import synchronizer
|
|
24
25
|
from pipecatcloud._utils.auth_utils import requires_login
|
|
25
26
|
from pipecatcloud._utils.console_utils import console
|
|
@@ -34,7 +35,7 @@ from pipecatcloud.cli.config import (
|
|
|
34
35
|
auth_cli = typer.Typer(name="auth", help="Manage Pipecat Cloud credentials", no_args_is_help=True)
|
|
35
36
|
|
|
36
37
|
# Cloudflare blocks requests with default Python user-agent strings (error 1010).
|
|
37
|
-
USER_AGENT = "PipecatCloudCLI/
|
|
38
|
+
USER_AGENT = f"PipecatCloudCLI/{_cli_version}"
|
|
38
39
|
|
|
39
40
|
# Ports to try for the localhost OAuth callback server.
|
|
40
41
|
# Our audience is developers who may have services running on common ports.
|
|
@@ -105,7 +106,7 @@ async def _get_account_org(
|
|
|
105
106
|
async with aiohttp.ClientSession() as session:
|
|
106
107
|
async with session.get(
|
|
107
108
|
f"{API.construct_api_url('organization_path')}",
|
|
108
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
109
|
+
headers={"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT},
|
|
109
110
|
) as resp:
|
|
110
111
|
if resp.status == 200:
|
|
111
112
|
data = await resp.json()
|
|
@@ -9,6 +9,7 @@ from pipecatcloud._utils.deploy_utils import (
|
|
|
9
9
|
DeploymentPhase,
|
|
10
10
|
_format_elapsed,
|
|
11
11
|
_format_revision_line,
|
|
12
|
+
format_health_lines,
|
|
12
13
|
interpret_deployment_status,
|
|
13
14
|
)
|
|
14
15
|
|
|
@@ -476,3 +477,156 @@ class TestFormatElapsed:
|
|
|
476
477
|
ts = (datetime.now(timezone.utc) - timedelta(seconds=45)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
477
478
|
result = _format_elapsed(ts)
|
|
478
479
|
assert "s" in result
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class TestHealthLines:
|
|
483
|
+
"""Tests for format_health_lines rendering."""
|
|
484
|
+
|
|
485
|
+
def test_crashloopbackoff_with_oomkilled(self):
|
|
486
|
+
lines = format_health_lines(
|
|
487
|
+
{
|
|
488
|
+
"reason": "CrashLoopBackOff",
|
|
489
|
+
"lastTerminationReason": "OOMKilled",
|
|
490
|
+
"lastExitCode": 137,
|
|
491
|
+
"restartCount": 42,
|
|
492
|
+
"replicasStarted": 3,
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
assert len(lines) >= 1
|
|
496
|
+
assert "CrashLoopBackOff" in lines[0]
|
|
497
|
+
assert "OOMKilled" in lines[0]
|
|
498
|
+
assert "exit 137" in lines[0]
|
|
499
|
+
assert "42 restarts" in lines[0]
|
|
500
|
+
assert "3 replicas" in lines[0]
|
|
501
|
+
|
|
502
|
+
def test_error_with_exit_code(self):
|
|
503
|
+
lines = format_health_lines(
|
|
504
|
+
{
|
|
505
|
+
"reason": "Error",
|
|
506
|
+
"lastExitCode": 1,
|
|
507
|
+
"restartCount": 4,
|
|
508
|
+
"replicasStarted": 2,
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
assert "Error" in lines[0]
|
|
512
|
+
assert "exit 1" in lines[0]
|
|
513
|
+
assert "4 restarts" in lines[0]
|
|
514
|
+
|
|
515
|
+
def test_message_shows_last_line(self):
|
|
516
|
+
lines = format_health_lines(
|
|
517
|
+
{
|
|
518
|
+
"reason": "Error",
|
|
519
|
+
"lastExitCode": 1,
|
|
520
|
+
"restartCount": 1,
|
|
521
|
+
"replicasStarted": 1,
|
|
522
|
+
"message": "Traceback (most recent call last):\n File \"bot.py\"\nModuleNotFoundError: No module named 'foo'",
|
|
523
|
+
}
|
|
524
|
+
)
|
|
525
|
+
assert len(lines) == 2
|
|
526
|
+
assert "ModuleNotFoundError" in lines[1]
|
|
527
|
+
# Should NOT include the full traceback, just the last line
|
|
528
|
+
assert "Traceback" not in lines[1]
|
|
529
|
+
|
|
530
|
+
def test_image_pull_backoff(self):
|
|
531
|
+
lines = format_health_lines(
|
|
532
|
+
{
|
|
533
|
+
"reason": "ImagePullBackOff",
|
|
534
|
+
"restartCount": 0,
|
|
535
|
+
"replicasStarted": 2,
|
|
536
|
+
"message": 'Back-off pulling image "nonexistent:latest"',
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
assert "ImagePullBackOff" in lines[0]
|
|
540
|
+
# No restarts, so no restart text
|
|
541
|
+
assert "restarts" not in lines[0]
|
|
542
|
+
assert "nonexistent:latest" in lines[1]
|
|
543
|
+
|
|
544
|
+
def test_no_reason_no_restarts(self):
|
|
545
|
+
"""Health with no reason and no restarts produces no lines."""
|
|
546
|
+
lines = format_health_lines(
|
|
547
|
+
{
|
|
548
|
+
"restartCount": 0,
|
|
549
|
+
"replicasStarted": 1,
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
assert len(lines) == 0
|
|
553
|
+
|
|
554
|
+
def test_same_reason_and_termination_reason(self):
|
|
555
|
+
"""When reason == lastTerminationReason, don't duplicate."""
|
|
556
|
+
lines = format_health_lines(
|
|
557
|
+
{
|
|
558
|
+
"reason": "Error",
|
|
559
|
+
"lastTerminationReason": "Error",
|
|
560
|
+
"lastExitCode": 1,
|
|
561
|
+
"restartCount": 3,
|
|
562
|
+
"replicasStarted": 1,
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
# Should show "Error (exit 1)" not "Error (Error, exit 1)"
|
|
566
|
+
assert "Error (exit 1)" in lines[0]
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class TestRevisionLineWithHealth:
|
|
570
|
+
"""Tests that _format_revision_line includes health details."""
|
|
571
|
+
|
|
572
|
+
def test_revision_with_crash_health(self):
|
|
573
|
+
output = _format_revision_line(
|
|
574
|
+
"Current ",
|
|
575
|
+
{
|
|
576
|
+
"deploymentID": "17baf1ed-1234",
|
|
577
|
+
"phase": "Validating",
|
|
578
|
+
"readyReplicas": 0,
|
|
579
|
+
"health": {
|
|
580
|
+
"ready": False,
|
|
581
|
+
"state": "terminated",
|
|
582
|
+
"reason": "Error",
|
|
583
|
+
"lastExitCode": 1,
|
|
584
|
+
"lastTerminationReason": "Error",
|
|
585
|
+
"restartCount": 4,
|
|
586
|
+
"replicasStarted": 2,
|
|
587
|
+
"message": "ModuleNotFoundError: No module named 'foo'",
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
)
|
|
591
|
+
lines = output.split("\n")
|
|
592
|
+
assert len(lines) >= 2
|
|
593
|
+
assert "Validating" in lines[0]
|
|
594
|
+
assert "Error" in lines[1]
|
|
595
|
+
assert "ModuleNotFoundError" in lines[2]
|
|
596
|
+
|
|
597
|
+
def test_revision_with_healthy_state_no_extra_lines(self):
|
|
598
|
+
output = _format_revision_line(
|
|
599
|
+
"Current ",
|
|
600
|
+
{
|
|
601
|
+
"deploymentID": "abc12345-5678",
|
|
602
|
+
"phase": "Active",
|
|
603
|
+
"readyReplicas": 3,
|
|
604
|
+
"health": {
|
|
605
|
+
"ready": True,
|
|
606
|
+
"state": "running",
|
|
607
|
+
"restartCount": 0,
|
|
608
|
+
"replicasStarted": 3,
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
lines = output.split("\n")
|
|
613
|
+
assert len(lines) == 1 # No health detail lines for healthy containers
|
|
614
|
+
|
|
615
|
+
def test_revision_with_infra_issue(self):
|
|
616
|
+
output = _format_revision_line(
|
|
617
|
+
"Current ",
|
|
618
|
+
{
|
|
619
|
+
"deploymentID": "abc12345-5678",
|
|
620
|
+
"phase": "Validating",
|
|
621
|
+
"readyReplicas": 0,
|
|
622
|
+
"health": {
|
|
623
|
+
"ready": True,
|
|
624
|
+
"state": "running",
|
|
625
|
+
"restartCount": 0,
|
|
626
|
+
"replicasStarted": 2,
|
|
627
|
+
},
|
|
628
|
+
"hasInfrastructureIssue": True,
|
|
629
|
+
},
|
|
630
|
+
)
|
|
631
|
+
assert "Infrastructure issue" in output
|
|
632
|
+
assert "contact support" in output
|
|
@@ -94,7 +94,8 @@ class TestAPIPATDetection:
|
|
|
94
94
|
async def test_pat_sends_bearer_header(self, pat_client):
|
|
95
95
|
"""PAT should be sent as Bearer token in Authorization header."""
|
|
96
96
|
headers = pat_client._configure_headers()
|
|
97
|
-
assert headers
|
|
97
|
+
assert headers["Authorization"] == "Bearer pcc_pat_60ee796dc5bade735a3b0ef1b5730618"
|
|
98
|
+
assert headers["User-Agent"].startswith("PipecatCloudCLI/")
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
class TestUsePATCommand:
|
|
@@ -121,13 +122,14 @@ class TestUsePATCommand:
|
|
|
121
122
|
) as mock_get_org,
|
|
122
123
|
patch("pipecatcloud.cli.commands.auth.update_user_config") as mock_update,
|
|
123
124
|
patch("pipecatcloud.cli.commands.auth.console") as mock_console,
|
|
125
|
+
patch("pipecatcloud.cli.commands.auth.config", {"org": None}),
|
|
124
126
|
):
|
|
125
127
|
mock_get_org.return_value = ("my-org", "My Organization")
|
|
126
128
|
|
|
127
129
|
await _use_pat_impl("pcc_pat_60ee796dc5bade735a3b0ef1b5730618")
|
|
128
130
|
|
|
129
|
-
# Should verify token
|
|
130
|
-
mock_get_org.assert_called_once_with("pcc_pat_60ee796dc5bade735a3b0ef1b5730618")
|
|
131
|
+
# Should verify token (active_org=None when no org in config)
|
|
132
|
+
mock_get_org.assert_called_once_with("pcc_pat_60ee796dc5bade735a3b0ef1b5730618", None)
|
|
131
133
|
|
|
132
134
|
# Should store to config with cleared OAuth fields
|
|
133
135
|
mock_update.assert_called_once_with(
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright (c) 2025, Daily
|
|
3
|
-
#
|
|
4
|
-
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
import functools
|
|
8
|
-
|
|
9
|
-
from pipecatcloud._utils.console_utils import console
|
|
10
|
-
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
11
|
-
from pipecatcloud.cli.config import config
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def requires_login(func):
|
|
15
|
-
@functools.wraps(func)
|
|
16
|
-
def wrapper(*args, **kwargs):
|
|
17
|
-
org = config.get("org")
|
|
18
|
-
token = config.get("token")
|
|
19
|
-
if org is None or token is None:
|
|
20
|
-
console.error(
|
|
21
|
-
f"You are not logged in. Please run `{PIPECAT_CLI_NAME} auth login` first.",
|
|
22
|
-
)
|
|
23
|
-
return
|
|
24
|
-
return func(*args, **kwargs)
|
|
25
|
-
|
|
26
|
-
return wrapper
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|