pipecatcloud 1.0.0__tar.gz → 1.1.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-1.0.0 → pipecatcloud-1.1.0}/CHANGELOG.md +85 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/CLAUDE.md +1 -1
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/PKG-INFO +4 -4
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/README.md +2 -2
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/pyproject.toml +2 -2
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/auth_utils.py +3 -2
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/console_utils.py +193 -5
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/api.py +4 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/agent.py +186 -45
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/auth.py +47 -28
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/build.py +42 -11
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/deploy.py +95 -90
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/docker.py +20 -11
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/organizations.py +152 -53
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/regions.py +14 -2
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/secrets.py +83 -39
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/spend_limit.py +31 -16
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/config.py +3 -1
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/entry_point.py +19 -2
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/PKG-INFO +4 -4
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/SOURCES.txt +8 -0
- pipecatcloud-1.1.0/src/pipecatcloud.egg-info/scm_file_list.json +75 -0
- pipecatcloud-1.1.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
- pipecatcloud-1.1.0/tests/conftest.py +35 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_agent_sessions.py +17 -21
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_agent_stop.py +17 -12
- pipecatcloud-1.1.0/tests/test_build_id_display.py +58 -0
- pipecatcloud-1.1.0/tests/test_deploy_image_pull_secret.py +109 -0
- pipecatcloud-1.1.0/tests/test_exit_codes.py +111 -0
- pipecatcloud-1.1.0/tests/test_json_output.py +226 -0
- pipecatcloud-1.1.0/tests/test_organizations.py +103 -0
- pipecatcloud-1.1.0/tests/test_output_modes.py +179 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_pat_auth.py +7 -2
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_spend_limit.py +53 -17
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/uv.lock +338 -196
- pipecatcloud-1.0.0/tests/conftest.py +0 -17
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.claude/settings.json +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/python-compatibility.yml +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.gitignore +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/LICENSE +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/pytest.ini +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/setup.cfg +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/regions.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/__init__.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_agent_args.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_api_client.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_auth.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_build_utils.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_config_file.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_deploy_poll.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_deployment_status.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_max_session_duration.py +0 -0
- {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_regions.py +0 -0
|
@@ -5,6 +5,91 @@ 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
|
+
## [1.1.0] - 2026-08-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- New global `--output {rich,plain,json}` option for every `pipecat cloud`
|
|
13
|
+
command, also settable via the `PIPECAT_OUTPUT` environment variable or the
|
|
14
|
+
`output` config key. Note the option is placed before the subcommand
|
|
15
|
+
(`pipecat cloud --output json agent list`); in CI, setting `PIPECAT_OUTPUT`
|
|
16
|
+
once avoids ordering concerns. `rich` is the interactive default; `plain` is
|
|
17
|
+
line-oriented with no boxes and no truncation; `json` emits one
|
|
18
|
+
machine-readable JSON object on stdout with all human output on stderr.
|
|
19
|
+
When stdout is not a terminal the CLI now defaults to `plain`.
|
|
20
|
+
|
|
21
|
+
- Progress is now visible when output is piped or redirected. Long-running
|
|
22
|
+
operations (deploy rollout polling, cloud builds) print each status
|
|
23
|
+
transition as a timestamped line instead of rendering nothing outside a
|
|
24
|
+
terminal. Cloud-build polling additionally reports build status changes.
|
|
25
|
+
|
|
26
|
+
- Listings (`agent list`, `agent sessions`, `agent deployments`,
|
|
27
|
+
`secrets list`, `organizations list`, `organizations keys list`, `build
|
|
28
|
+
list`, `regions list`, and friends) render as tab-separated rows with
|
|
29
|
+
full, untruncated values in `plain` mode. Piped output previously fell
|
|
30
|
+
back to an 80-column table that silently ellipsized IDs.
|
|
31
|
+
|
|
32
|
+
- `agent status` and `agent deployments` now show `Build: <build id>` for
|
|
33
|
+
cloud-built deployments instead of `Image: N/A`. Cloud builds produce an
|
|
34
|
+
image managed by Pipecat Cloud, so the build ID is the reference to use
|
|
35
|
+
when checking which artifact a deployment is running.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **The CLI now exits non-zero on failure.** Previously every error path
|
|
40
|
+
exited `0` — including failed deploys and running commands while logged
|
|
41
|
+
out — so CI pipelines could not detect failures. Errors and user
|
|
42
|
+
cancellations exit `1`; usage errors and refused non-interactive prompts
|
|
43
|
+
exit `2`. Pipelines that depended on the old always-zero behavior will
|
|
44
|
+
now correctly fail.
|
|
45
|
+
|
|
46
|
+
- Commands that would show a confirmation prompt now fail fast with exit
|
|
47
|
+
code `2` when stdin is not a terminal, pointing at the bypass flag
|
|
48
|
+
(`--yes`/`--force`/`--skip`), instead of hanging or misbehaving in CI.
|
|
49
|
+
|
|
50
|
+
- `organizations list` and `organizations keys create` label their
|
|
51
|
+
organization columns **Organization Name** (the display name) and
|
|
52
|
+
**Organization ID** (the identifier `--organization` takes), matching
|
|
53
|
+
the dashboard. The values are unchanged, as is `--output json`, which
|
|
54
|
+
emits the server's own field names. Scripts scraping the previous
|
|
55
|
+
`Organization` / `Name` headers out of table output need updating. In
|
|
56
|
+
`organizations list`, the active-organization marker moved to its own
|
|
57
|
+
`Active` column, so the ID column holds nothing but the identifier.
|
|
58
|
+
|
|
59
|
+
- CLI messages call an organization an "organization" rather than a
|
|
60
|
+
"namespace". The Kubernetes namespace an organization maps to is an
|
|
61
|
+
implementation detail, so messages about missing agents, secret sets,
|
|
62
|
+
or accounts now name the organization. `organizations select` reports
|
|
63
|
+
an unmatched `--organization` value as an unknown organization ID and
|
|
64
|
+
points at `organizations list`.
|
|
65
|
+
|
|
66
|
+
### Deprecated
|
|
67
|
+
|
|
68
|
+
- `spend-limit show --json` still works but is now an alias for the global
|
|
69
|
+
`--output json` mode.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- `organizations keys create` no longer reports a new key as "Using as
|
|
74
|
+
default in local config" when it was not stored as the default. The
|
|
75
|
+
panel carries that note only when the key actually became the default.
|
|
76
|
+
|
|
77
|
+
## [1.0.1] - 2026-07-13
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- `pipecat cloud deploy` now verifies the image pull secret passed via
|
|
82
|
+
`--credentials` by looking it up in the secrets list, matching on both name
|
|
83
|
+
and type. Previously the check misread an API error response as "secret
|
|
84
|
+
exists", and a regular secret set with the same name also passed. A deploy
|
|
85
|
+
referencing a missing image pull secret now aborts with a clear error
|
|
86
|
+
instead of proceeding.
|
|
87
|
+
|
|
88
|
+
- Documentation links in CLI output now point to `docs.pipecat.ai` instead of
|
|
89
|
+
the retired `docs.pipecat.daily.co` domain. API error panels link directly
|
|
90
|
+
to the matching entry in the error codes reference, and the deploy and
|
|
91
|
+
image pull secret prompts link to the image pull secrets guide.
|
|
92
|
+
|
|
8
93
|
## [1.0.0] - 2026-06-19
|
|
9
94
|
|
|
10
95
|
### Added
|
|
@@ -8,7 +8,7 @@ Guidance for Claude Code working in this repo.
|
|
|
8
8
|
|
|
9
9
|
- Package root: `src/pipecatcloud/`
|
|
10
10
|
- CLI: registered as a Pipecat CLI extension via the `pipecat_cli.extensions:cloud` entry point (surfaced as `pipecat cloud`). There is no standalone entry point — no `[project.scripts]` and no `__main__`; the CLI runs only through the Pipecat CLI.
|
|
11
|
-
- Public docs: https://docs.pipecat.
|
|
11
|
+
- Public docs: https://docs.pipecat.ai/pipecat-cloud
|
|
12
12
|
|
|
13
13
|
## Toolchain
|
|
14
14
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Cloud hosting for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
|
-
Project-URL: Website, https://docs.pipecat.
|
|
6
|
+
Project-URL: Website, https://docs.pipecat.ai/pipecat-cloud
|
|
7
7
|
Keywords: pipecat,pipecat-ai,voice,ai,llm
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -25,7 +25,7 @@ Dynamic: license-file
|
|
|
25
25
|
<img alt="pipecat cloud" width="500px" height="auto" src="https://raw.githubusercontent.com/daily-co/pipecat-cloud/main/pipecat-cloud.png">
|
|
26
26
|
</div></h1>
|
|
27
27
|
|
|
28
|
-
[](https://docs.pipecat.
|
|
28
|
+
[](https://docs.pipecat.ai/pipecat-cloud)
|
|
29
29
|
[](https://pypi.org/project/pipecatcloud)
|
|
30
30
|
|
|
31
31
|
# Pipecat Cloud
|
|
@@ -41,7 +41,7 @@ Python module and CLI for interacting with [Pipecat Cloud](https://pipecat.daily
|
|
|
41
41
|
|
|
42
42
|
### Documentation
|
|
43
43
|
|
|
44
|
-
Documentation for Pipecat Cloud is available [here](https://docs.pipecat.
|
|
44
|
+
Documentation for Pipecat Cloud is available [here](https://docs.pipecat.ai/pipecat-cloud).
|
|
45
45
|
|
|
46
46
|
### Installation
|
|
47
47
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img alt="pipecat cloud" width="500px" height="auto" src="https://raw.githubusercontent.com/daily-co/pipecat-cloud/main/pipecat-cloud.png">
|
|
3
3
|
</div></h1>
|
|
4
4
|
|
|
5
|
-
[](https://docs.pipecat.
|
|
5
|
+
[](https://docs.pipecat.ai/pipecat-cloud)
|
|
6
6
|
[](https://pypi.org/project/pipecatcloud)
|
|
7
7
|
|
|
8
8
|
# Pipecat Cloud
|
|
@@ -18,7 +18,7 @@ Python module and CLI for interacting with [Pipecat Cloud](https://pipecat.daily
|
|
|
18
18
|
|
|
19
19
|
### Documentation
|
|
20
20
|
|
|
21
|
-
Documentation for Pipecat Cloud is available [here](https://docs.pipecat.
|
|
21
|
+
Documentation for Pipecat Cloud is available [here](https://docs.pipecat.ai/pipecat-cloud).
|
|
22
22
|
|
|
23
23
|
### Installation
|
|
24
24
|
|
|
@@ -41,7 +41,7 @@ dev = [
|
|
|
41
41
|
"pytest>=9.0.0,<10",
|
|
42
42
|
"pytest-asyncio>=1.0.0,<2",
|
|
43
43
|
"ruff>=0.12.11,<1",
|
|
44
|
-
"setuptools
|
|
44
|
+
"setuptools>=83.0.0,<84",
|
|
45
45
|
"setuptools_scm~=8.3.1",
|
|
46
46
|
# Needed to import/test pipecatcloud.agent (the optional pipecat surface).
|
|
47
47
|
"pipecat-ai>=1.0.0",
|
|
@@ -77,7 +77,7 @@ select = [
|
|
|
77
77
|
"**/__init__.py" = ["D104"]
|
|
78
78
|
|
|
79
79
|
[project.urls]
|
|
80
|
-
Website = "https://docs.pipecat.
|
|
80
|
+
Website = "https://docs.pipecat.ai/pipecat-cloud"
|
|
81
81
|
|
|
82
82
|
[tool.setuptools_scm]
|
|
83
83
|
local_scheme = "no-local-version"
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import functools
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
|
+
import typer
|
|
10
11
|
|
|
11
12
|
from pipecatcloud.__version__ import version
|
|
12
13
|
from pipecatcloud._utils.console_utils import console
|
|
@@ -46,7 +47,7 @@ def requires_login(func):
|
|
|
46
47
|
console.error(
|
|
47
48
|
f"You are not logged in. Please run `{PIPECAT_CLI_NAME} auth login` first.",
|
|
48
49
|
)
|
|
49
|
-
|
|
50
|
+
raise typer.Exit(1)
|
|
50
51
|
|
|
51
52
|
# When org is not set locally (e.g. PIPECAT_TOKEN without a config file),
|
|
52
53
|
# resolve the user's default organization from the API.
|
|
@@ -59,7 +60,7 @@ def requires_login(func):
|
|
|
59
60
|
"Could not determine your organization. "
|
|
60
61
|
f"Set PIPECAT_ORG or run `{PIPECAT_CLI_NAME} auth login`.",
|
|
61
62
|
)
|
|
62
|
-
|
|
63
|
+
raise typer.Exit(1)
|
|
63
64
|
|
|
64
65
|
return await func(*args, **kwargs)
|
|
65
66
|
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import statistics
|
|
9
|
+
import sys
|
|
8
10
|
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
9
12
|
|
|
10
13
|
from rich.console import Console
|
|
11
14
|
from rich.panel import Panel
|
|
@@ -13,6 +16,65 @@ from rich.panel import Panel
|
|
|
13
16
|
from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS, PIPECAT_CLI_NAME
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
class OutputMode(str, Enum):
|
|
20
|
+
"""How the CLI renders its output.
|
|
21
|
+
|
|
22
|
+
- ``rich``: panels, spinners, and tables (the interactive default)
|
|
23
|
+
- ``plain``: line-oriented output — no boxes, no spinners, no truncation
|
|
24
|
+
- ``json``: one machine-parseable JSON object on stdout; all human
|
|
25
|
+
output is redirected to stderr
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
rich = "rich"
|
|
29
|
+
plain = "plain"
|
|
30
|
+
json = "json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stdin_is_interactive() -> bool:
|
|
34
|
+
"""Whether we can prompt the user. Separate function so tests can patch it."""
|
|
35
|
+
try:
|
|
36
|
+
return sys.stdin.isatty()
|
|
37
|
+
except (AttributeError, ValueError):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _PlainStatus:
|
|
42
|
+
"""Drop-in replacement for rich's Status in non-interactive output.
|
|
43
|
+
|
|
44
|
+
Rich renders nothing from ``console.status(...)`` when stdout is not a
|
|
45
|
+
terminal, which left long-running operations (e.g. the up-to-600s deploy
|
|
46
|
+
poll loop) completely silent in CI. This sink prints each *distinct*
|
|
47
|
+
status message as a timestamped line instead, so piped output shows
|
|
48
|
+
every transition without spamming identical poll updates.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, console: "PipecatConsole", message: str | None = None):
|
|
52
|
+
self._console = console
|
|
53
|
+
self._last: str | None = None
|
|
54
|
+
self._stopped = False
|
|
55
|
+
if message:
|
|
56
|
+
self.update(message)
|
|
57
|
+
|
|
58
|
+
def update(self, status: str | None = None, **kwargs) -> None:
|
|
59
|
+
if not status or self._stopped or status == self._last:
|
|
60
|
+
return
|
|
61
|
+
self._last = status
|
|
62
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
63
|
+
self._console.print(rf"[dim]\[{timestamp}][/dim] {status}")
|
|
64
|
+
|
|
65
|
+
def start(self) -> None:
|
|
66
|
+
self._stopped = False
|
|
67
|
+
|
|
68
|
+
def stop(self) -> None:
|
|
69
|
+
self._stopped = True
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> "_PlainStatus":
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, *_exc) -> None:
|
|
75
|
+
self.stop()
|
|
76
|
+
|
|
77
|
+
|
|
16
78
|
def format_cents(cents: int | None) -> str:
|
|
17
79
|
"""Render a cents value as a USD dollar string (e.g. 1234 -> "$12.34").
|
|
18
80
|
|
|
@@ -28,6 +90,92 @@ def format_cents(cents: int | None) -> str:
|
|
|
28
90
|
|
|
29
91
|
|
|
30
92
|
class PipecatConsole(Console):
|
|
93
|
+
def __init__(self, *args, **kwargs):
|
|
94
|
+
super().__init__(*args, **kwargs)
|
|
95
|
+
# None means "auto": rich on a terminal, plain otherwise.
|
|
96
|
+
self._explicit_output_mode: OutputMode | None = None
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def output_mode(self) -> OutputMode:
|
|
100
|
+
if self._explicit_output_mode is not None:
|
|
101
|
+
return self._explicit_output_mode
|
|
102
|
+
return OutputMode.rich if self.is_terminal else OutputMode.plain
|
|
103
|
+
|
|
104
|
+
def set_output_mode(self, mode: OutputMode) -> None:
|
|
105
|
+
self._explicit_output_mode = mode
|
|
106
|
+
if mode == OutputMode.json:
|
|
107
|
+
# stdout is reserved for the single JSON payload; every human-facing
|
|
108
|
+
# write on this console moves to stderr so parsers see pure JSON.
|
|
109
|
+
self.file = sys.stderr
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def rich_output(self) -> bool:
|
|
113
|
+
return self.output_mode == OutputMode.rich
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def json_output(self) -> bool:
|
|
117
|
+
return self.output_mode == OutputMode.json
|
|
118
|
+
|
|
119
|
+
def output_json(self, data) -> None:
|
|
120
|
+
"""Write the single machine-readable JSON object to stdout.
|
|
121
|
+
|
|
122
|
+
In json mode this console's own writes go to stderr, so this is the
|
|
123
|
+
only thing that touches stdout — parsers get pure JSON.
|
|
124
|
+
"""
|
|
125
|
+
sys.stdout.write(json.dumps(data, indent=2, default=str) + "\n")
|
|
126
|
+
sys.stdout.flush()
|
|
127
|
+
|
|
128
|
+
def status(self, status, **kwargs): # pyright: ignore[reportIncompatibleMethodOverride]
|
|
129
|
+
# Returns rich's Status in rich mode and a line-printing _PlainStatus
|
|
130
|
+
# otherwise; both expose update()/stop()/context-manager.
|
|
131
|
+
if self.rich_output:
|
|
132
|
+
return super().status(status, **kwargs)
|
|
133
|
+
return _PlainStatus(self, str(status))
|
|
134
|
+
|
|
135
|
+
def require_interactive(self, flag_hint: str | None = "--yes") -> None:
|
|
136
|
+
"""Fail fast instead of letting a prompt hang or misbehave in CI.
|
|
137
|
+
|
|
138
|
+
Exits 2 (usage error) when a prompt would be shown but stdin is not a
|
|
139
|
+
terminal, pointing at the flag that skips the prompt when one exists.
|
|
140
|
+
"""
|
|
141
|
+
import typer
|
|
142
|
+
|
|
143
|
+
if not stdin_is_interactive():
|
|
144
|
+
remedy = (
|
|
145
|
+
f"Pass [bold]{flag_hint}[/bold] to run non-interactively."
|
|
146
|
+
if flag_hint
|
|
147
|
+
else "This command requires an interactive terminal."
|
|
148
|
+
)
|
|
149
|
+
self.error(f"Confirmation required but input is not a terminal.\n{remedy}")
|
|
150
|
+
raise typer.Exit(2)
|
|
151
|
+
|
|
152
|
+
def print(self, *objects, **kwargs):
|
|
153
|
+
# In non-rich modes, never hard-wrap plain text lines: wrapped IDs and
|
|
154
|
+
# URIs are as unparseable as truncated ones.
|
|
155
|
+
if not self.rich_output and objects and all(isinstance(o, str) for o in objects):
|
|
156
|
+
kwargs.setdefault("soft_wrap", True)
|
|
157
|
+
return super().print(*objects, **kwargs)
|
|
158
|
+
|
|
159
|
+
def print_records(
|
|
160
|
+
self,
|
|
161
|
+
columns: list[str],
|
|
162
|
+
rows: list[tuple],
|
|
163
|
+
title: str | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Plain-mode listing: tab-separated, full values, no boxes.
|
|
166
|
+
|
|
167
|
+
Rich tables ellipsize cell values at 80 columns when piped, which
|
|
168
|
+
silently corrupts IDs consumed by scripts. Rows are written straight to
|
|
169
|
+
the output stream — bypassing Rich's tab expansion and line wrapping —
|
|
170
|
+
so the output stays greppable and `cut`-able.
|
|
171
|
+
"""
|
|
172
|
+
if title:
|
|
173
|
+
self.print(title)
|
|
174
|
+
self.file.write("\t".join(columns) + "\n")
|
|
175
|
+
for row in rows:
|
|
176
|
+
self.file.write("\t".join("" if cell is None else str(cell) for cell in row) + "\n")
|
|
177
|
+
self.file.flush()
|
|
178
|
+
|
|
31
179
|
def success(
|
|
32
180
|
self,
|
|
33
181
|
message,
|
|
@@ -38,6 +186,16 @@ class PipecatConsole(Console):
|
|
|
38
186
|
if not title:
|
|
39
187
|
title = f"{PANEL_TITLE_SUCCESS}{f' - {title_extra}' if title_extra is not None else ''}"
|
|
40
188
|
|
|
189
|
+
if not self.rich_output:
|
|
190
|
+
if isinstance(message, str):
|
|
191
|
+
self.print(f"[bold green]{title}:[/bold green] {message}")
|
|
192
|
+
else:
|
|
193
|
+
self.print(f"[bold green]{title}[/bold green]")
|
|
194
|
+
self.print(message)
|
|
195
|
+
if subtitle:
|
|
196
|
+
self.print(subtitle)
|
|
197
|
+
return
|
|
198
|
+
|
|
41
199
|
self.print(
|
|
42
200
|
Panel(
|
|
43
201
|
message,
|
|
@@ -59,6 +217,16 @@ class PipecatConsole(Console):
|
|
|
59
217
|
if not title:
|
|
60
218
|
title = f"{PANEL_TITLE_ERROR}{f' - {title_extra}' if title_extra is not None else ''}"
|
|
61
219
|
|
|
220
|
+
if not self.rich_output:
|
|
221
|
+
if isinstance(message, str):
|
|
222
|
+
self.print(f"[bold red]{title}:[/bold red] {message}")
|
|
223
|
+
else:
|
|
224
|
+
self.print(f"[bold red]{title}[/bold red]")
|
|
225
|
+
self.print(message)
|
|
226
|
+
if subtitle:
|
|
227
|
+
self.print(subtitle)
|
|
228
|
+
return
|
|
229
|
+
|
|
62
230
|
self.print(
|
|
63
231
|
Panel(
|
|
64
232
|
message,
|
|
@@ -74,9 +242,16 @@ class PipecatConsole(Console):
|
|
|
74
242
|
self.print("[yellow]Cancelled by user[/yellow]")
|
|
75
243
|
|
|
76
244
|
def unauthorized(self):
|
|
245
|
+
message = (
|
|
246
|
+
"Unauthorized request / invalid user token.\n\n"
|
|
247
|
+
f"Please log in again using [bold cyan]{PIPECAT_CLI_NAME} auth login[/bold cyan]"
|
|
248
|
+
)
|
|
249
|
+
if not self.rich_output:
|
|
250
|
+
self.error(message, title=f"{PANEL_TITLE_ERROR} - Unauthorized (401)")
|
|
251
|
+
return
|
|
77
252
|
self.print(
|
|
78
253
|
Panel(
|
|
79
|
-
|
|
254
|
+
message,
|
|
80
255
|
title=f"[bold red]{PANEL_TITLE_ERROR} - Unauthorized (401)[/bold red]",
|
|
81
256
|
subtitle="",
|
|
82
257
|
title_align="left",
|
|
@@ -112,13 +287,22 @@ class PipecatConsole(Console):
|
|
|
112
287
|
if not error_message:
|
|
113
288
|
hide_subtitle = True
|
|
114
289
|
|
|
290
|
+
panel_title = f"{PANEL_TITLE_ERROR}{f' - {code}' if code else ''}"
|
|
291
|
+
subtitle = (
|
|
292
|
+
f"[dim]Docs: https://docs.pipecat.ai/pipecat-cloud/fundamentals/error-codes#{str(code).lower()}[/dim]"
|
|
293
|
+
if not hide_subtitle and code
|
|
294
|
+
else None
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if not self.rich_output:
|
|
298
|
+
self.error(f"{title}: {error_message}", title=panel_title, subtitle=subtitle)
|
|
299
|
+
return
|
|
300
|
+
|
|
115
301
|
self.print(
|
|
116
302
|
Panel(
|
|
117
303
|
f"[red]{title}[/red]\n\n[dim]Error message:[/dim]\n{error_message}",
|
|
118
|
-
title=f"[bold red]{
|
|
119
|
-
subtitle=
|
|
120
|
-
if not hide_subtitle and code
|
|
121
|
-
else None,
|
|
304
|
+
title=f"[bold red]{panel_title}[/bold red]",
|
|
305
|
+
subtitle=subtitle,
|
|
122
306
|
title_align="left",
|
|
123
307
|
subtitle_align="left",
|
|
124
308
|
border_style="red",
|
|
@@ -158,6 +342,10 @@ class PipecatConsole(Console):
|
|
|
158
342
|
f"Or visit [link={dashboard}]{dashboard}[/link]"
|
|
159
343
|
)
|
|
160
344
|
|
|
345
|
+
if not self.rich_output:
|
|
346
|
+
self.error(body, title=f"{PANEL_TITLE_ERROR} - Spend limit reached (402)")
|
|
347
|
+
return
|
|
348
|
+
|
|
161
349
|
self.print(
|
|
162
350
|
Panel(
|
|
163
351
|
body,
|
|
@@ -197,6 +197,10 @@ class _API:
|
|
|
197
197
|
|
|
198
198
|
if not self.error:
|
|
199
199
|
return
|
|
200
|
+
# In json mode the machine-readable error goes to stdout; the human
|
|
201
|
+
# rendering below lands on stderr (the console is redirected there).
|
|
202
|
+
if self.is_cli and getattr(console, "json_output", False):
|
|
203
|
+
console.output_json({"error": self.error})
|
|
200
204
|
if isinstance(self.error, dict) and self.error.get("code", "400") == "401":
|
|
201
205
|
console.unauthorized()
|
|
202
206
|
else:
|