pipecatcloud 1.4.0__tar.gz → 1.5.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.4.0 → pipecatcloud-1.5.0}/CHANGELOG.md +65 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/CLAUDE.md +1 -0
- {pipecatcloud-1.4.0/src/pipecatcloud.egg-info → pipecatcloud-1.5.0}/PKG-INFO +5 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/README.md +4 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/pyproject.toml +1 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/build_utils.py +11 -4
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/deploy_utils.py +2 -1
- pipecatcloud-1.5.0/src/pipecatcloud/_utils/log_utils.py +42 -0
- pipecatcloud-1.5.0/src/pipecatcloud/_utils/regions.py +99 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/api.py +183 -4
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/agent.py +6 -8
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/auth.py +4 -3
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/deploy.py +7 -10
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/organizations.py +2 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/secrets.py +244 -51
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/entry_point.py +7 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/constants.py +1 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0/src/pipecatcloud.egg-info}/PKG-INFO +5 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud.egg-info/SOURCES.txt +5 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud.egg-info/scm_file_list.json +5 -0
- pipecatcloud-1.5.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_api_client.py +2 -0
- pipecatcloud-1.5.0/tests/test_log_redaction.py +472 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_regions.py +0 -57
- pipecatcloud-1.5.0/tests/test_secrets_bulk_write.py +450 -0
- pipecatcloud-1.5.0/tests/test_secrets_keys.py +244 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_secrets_reference.py +4 -8
- pipecatcloud-1.5.0/tests/test_secrets_retry.py +550 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/uv.lock +147 -142
- pipecatcloud-1.4.0/src/pipecatcloud/_utils/regions.py +0 -89
- pipecatcloud-1.4.0/src/pipecatcloud.egg-info/scm_version.json +0 -8
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.claude/settings.json +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.claude/skills/update-docs/SOURCE_DOC_MAPPING.md +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.github/workflows/python-compatibility.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.github/workflows/update-docs.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.gitignore +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/LICENSE +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/pytest.ini +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/scripts/release-changelog.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/setup.cfg +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/_utils/github_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/agent_profiles.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/build.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/docker.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/github.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/registry_keys.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/commands/spend_limit.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/conftest.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_agent_args.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_agent_profiles_and_resources.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_agent_status_output.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_auth.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_build_id_display.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_build_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_config_file.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_deploy_architecture.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_deploy_image_pull_secret.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_deploy_poll.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_deploy_referenced_secret_set.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_deployment_status.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_exit_codes.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_github_integration.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_json_output.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_max_session_duration.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_organizations.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_output_modes.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_pat_auth.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_regions_lifecycle.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_secrets_set_referenced.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.5.0}/tests/test_spend_limit.py +0 -0
|
@@ -5,6 +5,71 @@ 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.5.0] - 2026-11-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `secrets keys --region <region>` shows your organization's secret keys in a
|
|
13
|
+
region: each key's fingerprint, state, created and retired times, and how
|
|
14
|
+
many secrets it holds, as text or with `--output json`. The fingerprint is
|
|
15
|
+
the one a security notice names and, in a self-hosted region, the one your
|
|
16
|
+
cluster shows for the key. It never shows key material. Against a server
|
|
17
|
+
that does not offer this yet, it says so and exits 1.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `secrets set` with several values now writes them in one request, which the
|
|
22
|
+
server applies whole or not at all, so a large set takes one write in place
|
|
23
|
+
of one per key. Against a server that does not take this yet, including one
|
|
24
|
+
part way through an update, it writes one key at a time as before, and still
|
|
25
|
+
says how far it got if one fails.
|
|
26
|
+
- `secrets set`, `secrets unset` and `secrets image-pull-secret` now wait and
|
|
27
|
+
retry when the API answers a write with a 503 and a `Retry-After` of up to
|
|
28
|
+
30 seconds, as a region can on its first secrets write. They retry up to 3 times, say they
|
|
29
|
+
are waiting, and fail as before if the 503 persists. Other errors, and a 503
|
|
30
|
+
without a usable `Retry-After`, are not retried. A `Retry-After` above 30
|
|
31
|
+
seconds is not waited out; the error says how long the region asked for.
|
|
32
|
+
A write that ends in a 503 may still have reached the region, unless the
|
|
33
|
+
error says nothing was stored. Running the same command again settles it.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `--region` is now checked against the regions of the organization given
|
|
38
|
+
with `--organization`, not only the organization in your config file. This
|
|
39
|
+
affects `secrets set`, `secrets list`, `secrets image-pull-secret`,
|
|
40
|
+
`secrets reference`, `agent list` and `deploy`. A region that belongs only
|
|
41
|
+
to the other organization, such as a self-hosted region, was refused as
|
|
42
|
+
invalid.
|
|
43
|
+
- `secrets set` no longer reports success when a write gets no answer from the
|
|
44
|
+
server, as when the connection drops. It says so and exits 1.
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- Logs no longer contain secret values or other request data. With debug
|
|
49
|
+
logging on (`PIPECAT_CLI_LOG_LEVEL=DEBUG` for the CLI, `PCC_LOG_LEVEL=DEBUG`
|
|
50
|
+
for the Python package), `secrets set` wrote the values it was given to
|
|
51
|
+
stderr, and every API request wrote its body and query there, including
|
|
52
|
+
image pull credentials and session data. Requests now log their method,
|
|
53
|
+
path and how much they carry. Tokens were not in these lines. If you ran
|
|
54
|
+
with debug logging on and the output was kept, for example in CI logs,
|
|
55
|
+
change the values it contains.
|
|
56
|
+
- An unexpected error in the CLI could print its local variables in the
|
|
57
|
+
traceback, including secret values and your token. Tracebacks now show code
|
|
58
|
+
only. To see them as before, set `TYPER_STANDARD_TRACEBACK` to an empty value.
|
|
59
|
+
- A failed build context upload now logs its status and S3 error code, not
|
|
60
|
+
S3's error document, which can repeat signed upload details. This line is
|
|
61
|
+
logged at the default level.
|
|
62
|
+
- `secrets set` no longer shows the first characters of each value when it asks
|
|
63
|
+
you to confirm. It shows a fixed mask, the same for any value. Without
|
|
64
|
+
`--skip` and without a terminal, as in CI, it printed those characters and
|
|
65
|
+
then failed; it now fails first. If such output was kept, change the values
|
|
66
|
+
it contains.
|
|
67
|
+
- The locked versions of `virtualenv`, `packaging`, `distlib`, `Pillow` and
|
|
68
|
+
`setuptools` are upgraded for security fixes. These are the versions in
|
|
69
|
+
`uv.lock`, used to develop and test this package. What it requires when you
|
|
70
|
+
install it has not changed, so upgrade your own environment if it holds an
|
|
71
|
+
older one.
|
|
72
|
+
|
|
8
73
|
## [1.4.0] - 2026-10-22
|
|
9
74
|
|
|
10
75
|
### Added
|
|
@@ -103,6 +103,7 @@ pcc-deploy.toml Example deployment config consumed by `pipecat cloud de
|
|
|
103
103
|
- Auth uses OAuth2 with PKCE (RFC 7636). OAuth endpoints are discovered from the API server, not hardcoded. The callback server tries ports 8400-8404.
|
|
104
104
|
- The session argument types in `agent.py` subclass pipecat-ai's runner types and need pipecat-ai (>= 1.0.0); there are no fallback definitions. `MOQSessionArguments` needs 1.12.0 (`MOQRunnerArguments.relay_url`), so it is imported and defined under its own guard: on older pipecat-ai the module still imports and only that name is missing. It stays out of `__all__` for the same reason.
|
|
105
105
|
- 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.
|
|
106
|
+
- Never log a request or response body, a header, or an exception's text: they can hold secret values and tokens. Use `_utils/log_utils.py`. `tests/test_log_redaction.py` fails on a log call that reads a name or attribute it knows can hold one, however it is formatted, or that uses `logger.opt`, `logger.exception` or `logger.catch`. Only `describe_*` and `len` calls are exempt.
|
|
106
107
|
|
|
107
108
|
## Release flow
|
|
108
109
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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.ai/pipecat-cloud
|
|
@@ -70,6 +70,10 @@ To use Pipecat Cloud programmatically (without the CLI), install the package dir
|
|
|
70
70
|
|
|
71
71
|
4. Build and deploy your agent `pipecat cloud deploy`
|
|
72
72
|
|
|
73
|
+
### Seeing your secret keys
|
|
74
|
+
|
|
75
|
+
`pipecat cloud secrets keys --region <region>` shows your organization's secret keys in a region: each key's fingerprint, state, created and retired times, and how many secrets it holds. The fingerprint is the one a security notice names and, in a self-hosted region, the one your cluster shows for the key, so you can compare them. Add `--output json` before `secrets` for a single JSON document. Key material is never shown.
|
|
76
|
+
|
|
73
77
|
### Usage in Python scripts
|
|
74
78
|
|
|
75
79
|
If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.session` module.
|
|
@@ -47,6 +47,10 @@ To use Pipecat Cloud programmatically (without the CLI), install the package dir
|
|
|
47
47
|
|
|
48
48
|
4. Build and deploy your agent `pipecat cloud deploy`
|
|
49
49
|
|
|
50
|
+
### Seeing your secret keys
|
|
51
|
+
|
|
52
|
+
`pipecat cloud secrets keys --region <region>` shows your organization's secret keys in a region: each key's fingerprint, state, created and retired times, and how many secrets it holds. The fingerprint is the one a security notice names and, in a self-hosted region, the one your cluster shows for the key, so you can compare them. Add `--output json` before `secrets` for a single JSON document. Key material is never shown.
|
|
53
|
+
|
|
50
54
|
### Usage in Python scripts
|
|
51
55
|
|
|
52
56
|
If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.session` module.
|
|
@@ -17,6 +17,7 @@ import gzip
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import io
|
|
19
19
|
import os
|
|
20
|
+
import re
|
|
20
21
|
import tarfile
|
|
21
22
|
import time
|
|
22
23
|
from collections.abc import Callable
|
|
@@ -26,6 +27,8 @@ from pathlib import Path
|
|
|
26
27
|
import aiohttp
|
|
27
28
|
from loguru import logger
|
|
28
29
|
|
|
30
|
+
from pipecatcloud._utils.log_utils import describe_exception
|
|
31
|
+
|
|
29
32
|
# Default patterns to exclude from build context
|
|
30
33
|
DEFAULT_EXCLUSIONS: set[str] = {
|
|
31
34
|
# Version control
|
|
@@ -189,7 +192,7 @@ def load_dockerignore(context_dir: Path) -> set[str] | None:
|
|
|
189
192
|
if normalized:
|
|
190
193
|
patterns.add(normalized)
|
|
191
194
|
except Exception as e:
|
|
192
|
-
logger.warning(f"Failed to read .dockerignore: {e}")
|
|
195
|
+
logger.warning(f"Failed to read .dockerignore: {describe_exception(e)}")
|
|
193
196
|
return None
|
|
194
197
|
|
|
195
198
|
return patterns
|
|
@@ -375,11 +378,15 @@ async def upload_to_s3(
|
|
|
375
378
|
logger.debug(f"Upload successful: {response.status}")
|
|
376
379
|
return True
|
|
377
380
|
else:
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
# S3's error document can echo the signed policy and the
|
|
382
|
+
# credential it was given, so only its code is logged.
|
|
383
|
+
code = re.search(r"<Code>(\w+)</Code>", await response.text())
|
|
384
|
+
logger.error(
|
|
385
|
+
f"Upload failed: {response.status}{f' ({code[1]})' if code else ''}"
|
|
386
|
+
)
|
|
380
387
|
return False
|
|
381
388
|
except Exception as e:
|
|
382
|
-
logger.error(f"Upload error: {e}")
|
|
389
|
+
logger.error(f"Upload error: {describe_exception(e)}")
|
|
383
390
|
return False
|
|
384
391
|
|
|
385
392
|
|
|
@@ -17,6 +17,7 @@ import typer
|
|
|
17
17
|
from attr import dataclass, field
|
|
18
18
|
from loguru import logger
|
|
19
19
|
|
|
20
|
+
from pipecatcloud._utils.log_utils import describe_exception
|
|
20
21
|
from pipecatcloud.cli import PIPECAT_DEPLOY_CONFIG_PATH
|
|
21
22
|
from pipecatcloud.constants import KRISP_VIVA_MODELS, KrispVivaAudioFilter
|
|
22
23
|
from pipecatcloud.exception import ConfigFileError
|
|
@@ -607,7 +608,7 @@ def load_deploy_config_file() -> DeployConfigParams | None:
|
|
|
607
608
|
return validated_config
|
|
608
609
|
|
|
609
610
|
except Exception as e:
|
|
610
|
-
logger.debug(e)
|
|
611
|
+
logger.debug(describe_exception(e))
|
|
611
612
|
raise ConfigFileError(str(e))
|
|
612
613
|
|
|
613
614
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
What a log line may say about a request, a response or a failure.
|
|
9
|
+
|
|
10
|
+
Request and response bodies can hold secret values and credentials, under any name
|
|
11
|
+
and at any depth, and an exception's text can quote them. So a log line says how
|
|
12
|
+
much is there and what kind of thing it is, and never quotes any of it. Headers are
|
|
13
|
+
not logged at all: they carry the token.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def describe_url(url: str) -> str:
|
|
18
|
+
"""A URL without its query string, whose values are not for a log."""
|
|
19
|
+
return url.split("?", 1)[0]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def describe_body(body: object) -> str:
|
|
23
|
+
"""How much a request or response body holds, and never what."""
|
|
24
|
+
if body is None:
|
|
25
|
+
return "none"
|
|
26
|
+
if isinstance(body, dict | list | tuple):
|
|
27
|
+
return f"{type(body).__name__} of {len(body)}"
|
|
28
|
+
return type(body).__name__
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def describe_exception(exc: BaseException) -> str:
|
|
32
|
+
"""An exception's type, and its HTTP status if it has one, without its text."""
|
|
33
|
+
status = getattr(exc, "status", None)
|
|
34
|
+
return (
|
|
35
|
+
f"{type(exc).__name__} (status {status})" if isinstance(status, int) else type(exc).__name__
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def describe_api_error(error: object) -> str:
|
|
40
|
+
"""The code of an error body from the API, without its text."""
|
|
41
|
+
code = error.get("code") if isinstance(error, dict) else None
|
|
42
|
+
return f"code {code}" if code else "no code"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Region utilities for Pipecat Cloud CLI.
|
|
9
|
+
|
|
10
|
+
Provides functions to fetch and cache available regions from the API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pipecatcloud.cli.api import API
|
|
14
|
+
from pipecatcloud.cli.config import config
|
|
15
|
+
|
|
16
|
+
# Module-level cache of region lists, keyed by organization. Each organization
|
|
17
|
+
# has its own list (a self-hosted region is visible only to its organization),
|
|
18
|
+
# so one list must never answer for another.
|
|
19
|
+
_regions_cache: dict[str, list[dict[str, str]]] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get_regions(org: str | None = None) -> list[dict[str, str]]:
|
|
23
|
+
"""
|
|
24
|
+
Fetch available regions for an organization from the API with caching.
|
|
25
|
+
|
|
26
|
+
The list is cached per organization, so the API is called once per
|
|
27
|
+
organization per CLI session.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
org: Organization to list regions for. Defaults to the organization in
|
|
31
|
+
the config file.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of region objects with 'code' and 'display_name' fields, or an
|
|
35
|
+
empty list when the list cannot be fetched.
|
|
36
|
+
Example: [
|
|
37
|
+
{"code": "us-west", "display_name": "US West"},
|
|
38
|
+
{"code": "eu-central", "display_name": "EU Central"}
|
|
39
|
+
]
|
|
40
|
+
"""
|
|
41
|
+
global _regions_cache
|
|
42
|
+
|
|
43
|
+
org = org or config.get("org")
|
|
44
|
+
if _regions_cache is None:
|
|
45
|
+
_regions_cache = {}
|
|
46
|
+
|
|
47
|
+
if org not in _regions_cache:
|
|
48
|
+
# API returns (data, error) tuple
|
|
49
|
+
data, error = await API.regions(org=org)
|
|
50
|
+
if error or not data:
|
|
51
|
+
return []
|
|
52
|
+
_regions_cache[org] = data
|
|
53
|
+
|
|
54
|
+
return _regions_cache[org]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def region_error(region: str, org: str | None = None) -> str | None:
|
|
58
|
+
"""
|
|
59
|
+
Check a region code against the regions of the organization in use.
|
|
60
|
+
|
|
61
|
+
Fetches the list once. Returns None when the region is valid, otherwise a
|
|
62
|
+
message for the user. When the list cannot be fetched, the message says so
|
|
63
|
+
rather than calling the region invalid. The API client prints its own error
|
|
64
|
+
for the failed call before this message is returned (in json mode that error
|
|
65
|
+
goes to stdout as the error object), so the caller shows both: why the call
|
|
66
|
+
failed, then that the command stopped.
|
|
67
|
+
"""
|
|
68
|
+
org = org or config.get("org")
|
|
69
|
+
regions = await get_regions(org)
|
|
70
|
+
if not regions:
|
|
71
|
+
return (
|
|
72
|
+
f"Could not list the regions of organization '{org}', so region "
|
|
73
|
+
f"'{region}' could not be checked. Not continuing."
|
|
74
|
+
)
|
|
75
|
+
codes = [r["code"] for r in regions]
|
|
76
|
+
if region in codes:
|
|
77
|
+
return None
|
|
78
|
+
return f"Invalid region '{region}'. Valid regions are: {', '.join(codes)}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_architecture_for_region(
|
|
82
|
+
architecture: str, region: str, regions: list[dict]
|
|
83
|
+
) -> str | None:
|
|
84
|
+
"""Pre-validate an architecture choice against a region's capability
|
|
85
|
+
(PCC-1105). Returns an error message, or None when the choice is valid
|
|
86
|
+
or cannot be checked locally.
|
|
87
|
+
|
|
88
|
+
The regions payload carries ``supported_architectures`` only on newer
|
|
89
|
+
APIs — when absent, defer to the server's own validation (its 400 names
|
|
90
|
+
the supported list).
|
|
91
|
+
"""
|
|
92
|
+
entry = next((r for r in regions if r.get("code") == region), None)
|
|
93
|
+
supported = (entry or {}).get("supported_architectures")
|
|
94
|
+
if not supported or architecture in supported:
|
|
95
|
+
return None
|
|
96
|
+
return (
|
|
97
|
+
f"Architecture '{architecture}' is not supported in region '{region}' "
|
|
98
|
+
f"(supported: {', '.join(supported)})"
|
|
99
|
+
)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import json
|
|
8
9
|
import time
|
|
9
10
|
from collections.abc import Callable
|
|
@@ -15,6 +16,7 @@ from loguru import logger
|
|
|
15
16
|
|
|
16
17
|
from pipecatcloud.__version__ import version
|
|
17
18
|
from pipecatcloud._utils.deploy_utils import DeployConfigParams
|
|
19
|
+
from pipecatcloud._utils.log_utils import describe_body, describe_exception, describe_url
|
|
18
20
|
from pipecatcloud.config import config
|
|
19
21
|
from pipecatcloud.exception import AgentStartError
|
|
20
22
|
|
|
@@ -33,6 +35,41 @@ def api_method(func):
|
|
|
33
35
|
return wrapper
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
# A secrets write can be refused with a 503 and `Retry-After` while the region
|
|
39
|
+
# is not ready to store secrets yet (a self-hosted region's first write), while
|
|
40
|
+
# the server is busy with other writes, or when the region's answer was lost and
|
|
41
|
+
# the write may have landed. Repeating the same write is right for all of them.
|
|
42
|
+
# The CLI waits and repeats the request, bounded so a region that stays unready
|
|
43
|
+
# fails rather than hanging. Three retries cover a short warm-up (the server
|
|
44
|
+
# asks for about 5 seconds) with room to spare. The waits total at most
|
|
45
|
+
# 3 x 30 seconds; each of the 4 attempts can also take up to the client's own
|
|
46
|
+
# request timeout, so the worst case is those waits plus 4 timeouts. A larger
|
|
47
|
+
# `Retry-After` than the cap means the server wants longer than we are willing
|
|
48
|
+
# to wait. The request is not retried, and not clamped either: repeating it
|
|
49
|
+
# before the time the server named goes against the header. The error says so.
|
|
50
|
+
SECRETS_WRITE_MAX_RETRIES = 3
|
|
51
|
+
SECRETS_WRITE_MAX_RETRY_AFTER = 30
|
|
52
|
+
|
|
53
|
+
# How the API says what it takes. A server that writes several fields of a secret
|
|
54
|
+
# set in one request names this capability on its secrets responses, so the CLI
|
|
55
|
+
# knows without trying. One that does not is written one field at a time.
|
|
56
|
+
CAPABILITIES_HEADER = "Pipecat-Cloud-Capabilities"
|
|
57
|
+
SECRETS_BULK_WRITE_CAPABILITY = "secrets-bulk-write"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_retry_after(value: str | None) -> int | None:
|
|
61
|
+
"""Seconds from a `Retry-After` header, or None when absent, negative, or not
|
|
62
|
+
a whole number of seconds (the HTTP-date form is not honoured). A value above
|
|
63
|
+
the cap is returned as sent; the caller decides not to wait it out."""
|
|
64
|
+
if value is None:
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
seconds = int(value.strip())
|
|
68
|
+
except ValueError:
|
|
69
|
+
return None
|
|
70
|
+
return seconds if seconds >= 0 else None
|
|
71
|
+
|
|
72
|
+
|
|
36
73
|
class _API:
|
|
37
74
|
def __init__(self, token: str | None = None, is_cli: bool = False):
|
|
38
75
|
self.token = token
|
|
@@ -43,6 +80,15 @@ class _API:
|
|
|
43
80
|
# carries a code of its own), so a caller that decides on the status
|
|
44
81
|
# reads it here. None when the request got no response at all.
|
|
45
82
|
self.error_status: int | None = None
|
|
83
|
+
# The `Retry-After` header of the last failed response, as sent, or
|
|
84
|
+
# None when it had none.
|
|
85
|
+
self.error_retry_after: str | None = None
|
|
86
|
+
# The capabilities header of the last response, as sent, or None when it
|
|
87
|
+
# had none.
|
|
88
|
+
self.capabilities_header: str | None = None
|
|
89
|
+
# Whether the server said, on the last `secrets_get`, that it takes
|
|
90
|
+
# several fields of a set in one write. False until it does.
|
|
91
|
+
self.secrets_bulk_write: bool = False
|
|
46
92
|
self.bubble_next = False
|
|
47
93
|
self.is_cli = is_cli
|
|
48
94
|
|
|
@@ -140,7 +186,12 @@ class _API:
|
|
|
140
186
|
logger.debug("Token refresh failed, proceeding with expired token")
|
|
141
187
|
|
|
142
188
|
async with aiohttp.ClientSession() as session:
|
|
143
|
-
|
|
189
|
+
# Every route comes through here, and a body or a query can hold secret
|
|
190
|
+
# values, so neither is logged, whichever route this is.
|
|
191
|
+
logger.debug(
|
|
192
|
+
f"Request: {method} {describe_url(url)} "
|
|
193
|
+
f"params: {describe_body(params)} body: {describe_body(json)}"
|
|
194
|
+
)
|
|
144
195
|
|
|
145
196
|
response = await session.request(
|
|
146
197
|
method=method,
|
|
@@ -149,6 +200,7 @@ class _API:
|
|
|
149
200
|
params=params,
|
|
150
201
|
json=json,
|
|
151
202
|
)
|
|
203
|
+
self.capabilities_header = response.headers.get(CAPABILITIES_HEADER)
|
|
152
204
|
|
|
153
205
|
if not response.ok:
|
|
154
206
|
logger.debug(f"Response not ok: {response.status} {response.reason}")
|
|
@@ -156,6 +208,7 @@ class _API:
|
|
|
156
208
|
return None
|
|
157
209
|
|
|
158
210
|
self.error_status = response.status
|
|
211
|
+
self.error_retry_after = response.headers.get("Retry-After")
|
|
159
212
|
# Extract PCC error code, where applicable
|
|
160
213
|
try:
|
|
161
214
|
# Try to parse the error as JSON
|
|
@@ -188,6 +241,7 @@ class _API:
|
|
|
188
241
|
async def wrapper(*args, live=None, **kwargs):
|
|
189
242
|
self.error = None
|
|
190
243
|
self.error_status = None
|
|
244
|
+
self.error_retry_after = None
|
|
191
245
|
try:
|
|
192
246
|
result = await method_func(*args, **kwargs)
|
|
193
247
|
self.bubble_next = False
|
|
@@ -203,7 +257,7 @@ class _API:
|
|
|
203
257
|
raise e
|
|
204
258
|
|
|
205
259
|
if self.error and not self.bubble_next:
|
|
206
|
-
logger.debug(e)
|
|
260
|
+
logger.debug(describe_exception(e))
|
|
207
261
|
self.print_error()
|
|
208
262
|
|
|
209
263
|
self.bubble_next = False
|
|
@@ -383,11 +437,24 @@ class _API:
|
|
|
383
437
|
Returns the full response object: {secrets, region, status, errorMessage?}.
|
|
384
438
|
Use this when you need readiness; use _secrets_list when you only need
|
|
385
439
|
the existence check or the flat keys array.
|
|
440
|
+
|
|
441
|
+
Also records whether the server takes several fields in one write, which
|
|
442
|
+
it says whether or not the set exists.
|
|
386
443
|
"""
|
|
387
444
|
url = f"{self.construct_api_url('secrets_path').format(org=org)}/{secret_set}"
|
|
445
|
+
self.capabilities_header = None
|
|
446
|
+
self.secrets_bulk_write = False
|
|
388
447
|
result = await self._base_request("GET", url, not_found_is_empty=True)
|
|
448
|
+
self.secrets_bulk_write = self.last_response_offers_bulk_write()
|
|
389
449
|
return result or None
|
|
390
450
|
|
|
451
|
+
def last_response_offers_bulk_write(self) -> bool:
|
|
452
|
+
"""Whether the last response came from a server that takes several fields in
|
|
453
|
+
one write. In a rolling update of the API, responses can differ on this."""
|
|
454
|
+
return SECRETS_BULK_WRITE_CAPABILITY in [
|
|
455
|
+
c.strip() for c in (self.capabilities_header or "").split(",")
|
|
456
|
+
]
|
|
457
|
+
|
|
391
458
|
@property
|
|
392
459
|
def secrets_get(self):
|
|
393
460
|
"""Get a single secret set with readiness status.
|
|
@@ -408,7 +475,70 @@ class _API:
|
|
|
408
475
|
if region:
|
|
409
476
|
data = {**data, "region": region}
|
|
410
477
|
|
|
411
|
-
|
|
478
|
+
# A PUT replaces the value of each key it names, so repeating it is safe.
|
|
479
|
+
result, _ = await self._secrets_write_with_retry("PUT", url, json=data)
|
|
480
|
+
return result or {}
|
|
481
|
+
|
|
482
|
+
async def _secrets_write_with_retry(
|
|
483
|
+
self, method: str, url: str, **kwargs
|
|
484
|
+
) -> tuple[dict | None, int]:
|
|
485
|
+
"""A secrets write, and how many times it was repeated. A 503 with
|
|
486
|
+
`Retry-After` is the server saying the write could not be done yet; wait and
|
|
487
|
+
repeat, a bounded number of times. Only for a write that is safe to repeat."""
|
|
488
|
+
attempt = 0
|
|
489
|
+
while True:
|
|
490
|
+
try:
|
|
491
|
+
return await self._base_request(method, url, **kwargs), attempt
|
|
492
|
+
except aiohttp.ClientResponseError as e:
|
|
493
|
+
wait = _parse_retry_after(self.error_retry_after)
|
|
494
|
+
too_long = wait is not None and wait > SECRETS_WRITE_MAX_RETRY_AFTER
|
|
495
|
+
if (
|
|
496
|
+
e.status != 503
|
|
497
|
+
or wait is None
|
|
498
|
+
or too_long
|
|
499
|
+
or attempt >= SECRETS_WRITE_MAX_RETRIES
|
|
500
|
+
):
|
|
501
|
+
if e.status == 503 and isinstance(self.error, dict):
|
|
502
|
+
notes = []
|
|
503
|
+
if attempt:
|
|
504
|
+
# Say the retries happened, so the user does not
|
|
505
|
+
# think the first refusal was final.
|
|
506
|
+
noun = "retry" if attempt == 1 else "retries"
|
|
507
|
+
notes.append(f"Still unavailable after {attempt} {noun}.")
|
|
508
|
+
if too_long:
|
|
509
|
+
# Waiting less than the server named would go
|
|
510
|
+
# against the header, so say why nothing more was retried.
|
|
511
|
+
notes.append(
|
|
512
|
+
f"The region asked to wait {wait} seconds, longer than the "
|
|
513
|
+
f"CLI will wait ({SECRETS_WRITE_MAX_RETRY_AFTER}). "
|
|
514
|
+
"Run the command again after that."
|
|
515
|
+
)
|
|
516
|
+
if notes:
|
|
517
|
+
# Same fallback the console uses to show an error.
|
|
518
|
+
text = (
|
|
519
|
+
self.error.get("error")
|
|
520
|
+
or self.error.get("message")
|
|
521
|
+
or "Service unavailable."
|
|
522
|
+
)
|
|
523
|
+
self.error = {**self.error, "error": f"{text} {' '.join(notes)}"}
|
|
524
|
+
raise
|
|
525
|
+
attempt += 1
|
|
526
|
+
self._notify_retry(wait, attempt)
|
|
527
|
+
await asyncio.sleep(wait)
|
|
528
|
+
self.error = None
|
|
529
|
+
self.error_status = None
|
|
530
|
+
self.error_retry_after = None
|
|
531
|
+
|
|
532
|
+
def _notify_retry(self, wait: int, attempt: int) -> None:
|
|
533
|
+
"""Tell the CLI user the request is waiting to be repeated, so a pause is not silent."""
|
|
534
|
+
if not self.is_cli:
|
|
535
|
+
return
|
|
536
|
+
from pipecatcloud._utils.console_utils import console
|
|
537
|
+
|
|
538
|
+
console.print(
|
|
539
|
+
f"[yellow]The write could not be completed yet. Retrying in {wait}s "
|
|
540
|
+
f"(attempt {attempt} of {SECRETS_WRITE_MAX_RETRIES})...[/yellow]"
|
|
541
|
+
)
|
|
412
542
|
|
|
413
543
|
@property
|
|
414
544
|
def secrets_upsert(self):
|
|
@@ -421,6 +551,47 @@ class _API:
|
|
|
421
551
|
"""
|
|
422
552
|
return self.create_api_method(self._secrets_upsert)
|
|
423
553
|
|
|
554
|
+
async def _secrets_upsert_fields(
|
|
555
|
+
self, fields: dict[str, str], set_name: str, org: str, region: str | None = None
|
|
556
|
+
) -> dict:
|
|
557
|
+
return await self._secrets_upsert(
|
|
558
|
+
data={
|
|
559
|
+
"secrets": [{"secretKey": key, "secretValue": val} for key, val in fields.items()]
|
|
560
|
+
},
|
|
561
|
+
set_name=set_name,
|
|
562
|
+
org=org,
|
|
563
|
+
region=region,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def secrets_upsert_fields(self):
|
|
568
|
+
"""Create / modify several fields of a secret set in one write, which the
|
|
569
|
+
server applies whole or not at all. Only for a server that says it takes
|
|
570
|
+
this (`secrets_bulk_write`, after `secrets_get`).
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
fields: the keys and values to write
|
|
574
|
+
set_name: name of set to create or update
|
|
575
|
+
org: Organization ID
|
|
576
|
+
region: region for secret set
|
|
577
|
+
"""
|
|
578
|
+
return self.create_api_method(self._secrets_upsert_fields)
|
|
579
|
+
|
|
580
|
+
async def _secrets_keys(self, org: str, region: str) -> dict | None:
|
|
581
|
+
url = f"{self.construct_api_url('regions_path').format(org=org)}/{region}/secret-keys"
|
|
582
|
+
return await self._base_request("GET", url, not_found_is_empty=True)
|
|
583
|
+
|
|
584
|
+
@property
|
|
585
|
+
def secrets_keys(self):
|
|
586
|
+
"""The organization's secret keys in a region: each key's fingerprint, state
|
|
587
|
+
and times. None when the server does not offer this.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
org: Organization ID
|
|
591
|
+
region: region to read
|
|
592
|
+
"""
|
|
593
|
+
return self.create_api_method(self._secrets_keys)
|
|
594
|
+
|
|
424
595
|
async def _secrets_reference(self, name: str, region: str, org: str) -> dict:
|
|
425
596
|
url = f"{self.construct_api_url('secrets_path').format(org=org)}/references"
|
|
426
597
|
return await self._base_request("POST", url, json={"name": name, "region": region}) or {}
|
|
@@ -442,7 +613,15 @@ class _API:
|
|
|
442
613
|
|
|
443
614
|
async def _secrets_delete(self, set_name: str, secret_name: str, org: str) -> dict | None:
|
|
444
615
|
url = f"{self.construct_api_url('secrets_path').format(org=org)}/{set_name}/{secret_name}"
|
|
445
|
-
|
|
616
|
+
# Deleting a key twice leaves the same set, so it is repeated like a write.
|
|
617
|
+
result, retried = await self._secrets_write_with_retry(
|
|
618
|
+
"DELETE", url, not_found_is_empty=True
|
|
619
|
+
)
|
|
620
|
+
if result is None and retried:
|
|
621
|
+
# Not found once repeated: the attempt the server could not confirm
|
|
622
|
+
# removed it, or it was never there. Either way the key is gone.
|
|
623
|
+
return {"secretKey": secret_name}
|
|
624
|
+
return result
|
|
446
625
|
|
|
447
626
|
@property
|
|
448
627
|
def secrets_delete(self):
|
|
@@ -47,7 +47,8 @@ from pipecatcloud._utils.github_utils import (
|
|
|
47
47
|
ref_to_branch,
|
|
48
48
|
short_sha,
|
|
49
49
|
)
|
|
50
|
-
from pipecatcloud._utils.
|
|
50
|
+
from pipecatcloud._utils.log_utils import describe_body, describe_exception
|
|
51
|
+
from pipecatcloud._utils.regions import region_error
|
|
51
52
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
52
53
|
from pipecatcloud.cli.api import API
|
|
53
54
|
from pipecatcloud.cli.config import config
|
|
@@ -176,11 +177,8 @@ async def list_agents(
|
|
|
176
177
|
org = organization or config.get("org")
|
|
177
178
|
|
|
178
179
|
# Validate region if provided
|
|
179
|
-
if region and
|
|
180
|
-
|
|
181
|
-
console.print(
|
|
182
|
-
f"[red]Invalid region '{region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
|
|
183
|
-
)
|
|
180
|
+
if region and (region_message := await region_error(region, org=org)):
|
|
181
|
+
console.error(region_message)
|
|
184
182
|
raise typer.Exit(1)
|
|
185
183
|
|
|
186
184
|
with console.status(
|
|
@@ -269,7 +267,7 @@ async def status(
|
|
|
269
267
|
) as live:
|
|
270
268
|
data, error = await API.agent(agent_name=agent_name, org=org, live=live)
|
|
271
269
|
|
|
272
|
-
logger.debug(f"Agent status: {data}")
|
|
270
|
+
logger.debug(f"Agent status: {describe_body(data)}")
|
|
273
271
|
|
|
274
272
|
live.stop()
|
|
275
273
|
|
|
@@ -1117,7 +1115,7 @@ async def deployments(
|
|
|
1117
1115
|
)
|
|
1118
1116
|
)
|
|
1119
1117
|
except Exception as e:
|
|
1120
|
-
logger.debug(e)
|
|
1118
|
+
logger.debug(describe_exception(e))
|
|
1121
1119
|
console.api_error(error_code, f"Unable to get deployments for {agent_name}")
|
|
1122
1120
|
raise typer.Exit(1)
|
|
1123
1121
|
|