pipecatcloud 1.4.0__tar.gz → 1.6.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.6.0}/CHANGELOG.md +152 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/CLAUDE.md +1 -0
- {pipecatcloud-1.4.0/src/pipecatcloud.egg-info → pipecatcloud-1.6.0}/PKG-INFO +5 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/README.md +4 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/pyproject.toml +1 -1
- pipecatcloud-1.6.0/src/pipecatcloud/_utils/base_image.py +89 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/build_utils.py +11 -4
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/deploy_utils.py +3 -2
- pipecatcloud-1.6.0/src/pipecatcloud/_utils/environment_utils.py +136 -0
- pipecatcloud-1.6.0/src/pipecatcloud/_utils/log_utils.py +42 -0
- pipecatcloud-1.6.0/src/pipecatcloud/_utils/regions.py +99 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/api.py +411 -16
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/agent.py +200 -90
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/agent_profiles.py +3 -3
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/auth.py +4 -3
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/deploy.py +29 -25
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/docker.py +5 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/organizations.py +2 -1
- pipecatcloud-1.6.0/src/pipecatcloud/cli/commands/project.py +465 -0
- pipecatcloud-1.6.0/src/pipecatcloud/cli/commands/project_group.py +209 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/secrets.py +257 -51
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/entry_point.py +25 -7
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/config.py +10 -3
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/constants.py +1 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0/src/pipecatcloud.egg-info}/PKG-INFO +5 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud.egg-info/SOURCES.txt +13 -1
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud.egg-info/scm_file_list.json +12 -0
- pipecatcloud-1.6.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_agent_sessions.py +27 -3
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_api_client.py +187 -1
- pipecatcloud-1.6.0/tests/test_base_image.py +98 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_deploy_poll.py +15 -2
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_exit_codes.py +16 -0
- pipecatcloud-1.6.0/tests/test_log_redaction.py +472 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_organizations.py +28 -0
- pipecatcloud-1.6.0/tests/test_projects.py +929 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_regions.py +1 -57
- pipecatcloud-1.6.0/tests/test_secrets_bulk_write.py +455 -0
- pipecatcloud-1.6.0/tests/test_secrets_keys.py +244 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_secrets_reference.py +4 -8
- pipecatcloud-1.6.0/tests/test_secrets_retry.py +551 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_secrets_set_referenced.py +4 -0
- pipecatcloud-1.6.0/tests/test_unread_answers.py +153 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.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.6.0}/.claude/settings.json +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.claude/skills/update-docs/SOURCE_DOC_MAPPING.md +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.github/workflows/python-compatibility.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.github/workflows/update-docs.yml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.gitignore +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/LICENSE +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/pytest.ini +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/scripts/release-changelog.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/setup.cfg +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/_utils/github_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/build.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/github.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/registry_keys.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/commands/spend_limit.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/__init__.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/conftest.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_agent_args.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_agent_profiles_and_resources.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_agent_status_output.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_auth.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_build_id_display.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_build_utils.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_config_file.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_deploy_architecture.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_deploy_image_pull_secret.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_deploy_referenced_secret_set.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_deployment_status.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_github_integration.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_json_output.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_max_session_duration.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_output_modes.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_pat_auth.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_regions_lifecycle.py +0 -0
- {pipecatcloud-1.4.0 → pipecatcloud-1.6.0}/tests/test_spend_limit.py +0 -0
|
@@ -5,6 +5,158 @@ 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.6.0] - 2026-11-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `project` is the command group for everything you deploy and run. A project
|
|
13
|
+
is an agent with environments; each environment is an agent of its own, and
|
|
14
|
+
`--env` addresses one through its project.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pipecat cloud project list
|
|
18
|
+
pipecat cloud project status my-bot --env staging
|
|
19
|
+
pipecat cloud project logs my-bot --env pr-7
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every command that took an agent name takes `--env` as well: `status`,
|
|
23
|
+
`logs`, `sessions`, `deployments`, `delete`, `start`, `stop`, `link`,
|
|
24
|
+
`unlink` and `deploy`. A positional agent name still works, so
|
|
25
|
+
`project status my-bot-pr-7` resolves the same preview.
|
|
26
|
+
|
|
27
|
+
- `project env` manages a project's environments.
|
|
28
|
+
|
|
29
|
+
- `project env add <project> <name> --agent <agent>` links an existing agent
|
|
30
|
+
as an environment. `project env remove` unlinks it, leaving it running as
|
|
31
|
+
a project of its own. `project env list` lists them: `production`, which
|
|
32
|
+
is the project's own agent, then the named environments and then the PR
|
|
33
|
+
previews.
|
|
34
|
+
- `--env production` is the project itself, which is the environment it
|
|
35
|
+
doubles as, so it means the same as naming the project with no `--env`.
|
|
36
|
+
`env promote` and `env remove` name an environment to act on, so they
|
|
37
|
+
refuse `production` and say why.
|
|
38
|
+
- `project env promote <project> <name>` deploys an environment's running
|
|
39
|
+
build, or the image it was deployed from, onto the project. Code only: the
|
|
40
|
+
project keeps its own secrets, scaling and profile. It confirms first, and
|
|
41
|
+
`--force` skips the prompt.
|
|
42
|
+
|
|
43
|
+
- `project previews enable <project> --secret-set <set>` turns PR previews on,
|
|
44
|
+
with `--bot-pull-requests` to include pull requests opened by bots.
|
|
45
|
+
`project previews disable` turns them off and deletes the previews it has.
|
|
46
|
+
`project previews status` shows the setting and the open previews.
|
|
47
|
+
|
|
48
|
+
- `project watch-paths set <project> <pattern>...` sets the glob patterns a
|
|
49
|
+
change has to match to deploy, `clear` removes them, and `show` prints them. They are a
|
|
50
|
+
project setting: its environments and previews follow the same list.
|
|
51
|
+
|
|
52
|
+
- `project list --all` includes environments and PR previews. Without it the
|
|
53
|
+
listing is projects only, as the dashboard shows them. Both carry columns for
|
|
54
|
+
a project's environment count and preview setting, and `--all` names each
|
|
55
|
+
environment's project.
|
|
56
|
+
|
|
57
|
+
- `deploy`, when it runs a cloud build, and `docker build-push` warn when the
|
|
58
|
+
Dockerfile builds `FROM dailyco/pipecat-base:latest`, or from
|
|
59
|
+
`dailyco/pipecat-base` with no tag. That tag names no Python, runs Python
|
|
60
|
+
3.12, and stops building on 2027-04-05. The warning names the line, and the
|
|
61
|
+
tag that keeps the same Python: `dailyco/pipecat-base:latest-py3.12`.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- `agent` is **deprecated**. It is the same group under its old name: every
|
|
66
|
+
command goes on working and `agent list` goes on listing every agent, with a
|
|
67
|
+
notice on stderr pointing at `project`. Use `project`, and `--env` to address
|
|
68
|
+
an environment.
|
|
69
|
+
- `status` reports the project and environment of an agent that is one, its
|
|
70
|
+
watch paths, and, for a project, whether PR previews are on and whether bot
|
|
71
|
+
pull requests get them. For a PR preview it names the pull request.
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
- `agent scale` is gone. It was never implemented: it printed "Not implemented"
|
|
76
|
+
and exited. Set an agent's instance counts with `pipecat cloud deploy
|
|
77
|
+
--min-agents/--max-agents`, or the `[scaling]` section of `pcc-deploy.toml`.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- A command no longer reports an operation as done when the CLI never managed
|
|
82
|
+
to make the request. A mistake inside the client, such as a call made with a
|
|
83
|
+
name the API method does not take, was handed back as an empty result, and
|
|
84
|
+
every command reads an empty error as success: a build promotion printed
|
|
85
|
+
"Promoted build ..." having sent no request at all. Such a failure now ends
|
|
86
|
+
the command with a non-zero exit code.
|
|
87
|
+
- A command that cannot reach the API, or cannot read its answer, now says so
|
|
88
|
+
and exits 1. A dropped connection, a host that does not resolve, a request
|
|
89
|
+
that timed out and an answer that stops part way through were all read as an
|
|
90
|
+
empty answer, which most commands reported as success. `secrets set`
|
|
91
|
+
already said so, and still says whether a write it got no answer for may have
|
|
92
|
+
been stored. `deploy` and `github connect` still treat it as a blip while
|
|
93
|
+
they are waiting, and keep polling.
|
|
94
|
+
|
|
95
|
+
## [1.5.0] - 2026-11-05
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- `secrets keys --region <region>` shows your organization's secret keys in a
|
|
100
|
+
region: each key's fingerprint, state, created and retired times, and how
|
|
101
|
+
many secrets it holds, as text or with `--output json`. The fingerprint is
|
|
102
|
+
the one a security notice names and, in a self-hosted region, the one your
|
|
103
|
+
cluster shows for the key. It never shows key material. Against a server
|
|
104
|
+
that does not offer this yet, it says so and exits 1.
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
|
|
108
|
+
- `secrets set` with several values now writes them in one request, which the
|
|
109
|
+
server applies whole or not at all, so a large set takes one write in place
|
|
110
|
+
of one per key. Against a server that does not take this yet, including one
|
|
111
|
+
part way through an update, it writes one key at a time as before, and still
|
|
112
|
+
says how far it got if one fails.
|
|
113
|
+
- `secrets set`, `secrets unset` and `secrets image-pull-secret` now wait and
|
|
114
|
+
retry when the API answers a write with a 503 and a `Retry-After` of up to
|
|
115
|
+
30 seconds, as a region can on its first secrets write. They retry up to 3 times, say they
|
|
116
|
+
are waiting, and fail as before if the 503 persists. Other errors, and a 503
|
|
117
|
+
without a usable `Retry-After`, are not retried. A `Retry-After` above 30
|
|
118
|
+
seconds is not waited out; the error says how long the region asked for.
|
|
119
|
+
A write that ends in a 503 may still have reached the region, unless the
|
|
120
|
+
error says nothing was stored. Running the same command again settles it.
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
|
|
124
|
+
- `--region` is now checked against the regions of the organization given
|
|
125
|
+
with `--organization`, not only the organization in your config file. This
|
|
126
|
+
affects `secrets set`, `secrets list`, `secrets image-pull-secret`,
|
|
127
|
+
`secrets reference`, `agent list` and `deploy`. A region that belongs only
|
|
128
|
+
to the other organization, such as a self-hosted region, was refused as
|
|
129
|
+
invalid.
|
|
130
|
+
- `secrets set` no longer reports success when a write gets no answer from the
|
|
131
|
+
server, as when the connection drops. It says so and exits 1.
|
|
132
|
+
|
|
133
|
+
### Security
|
|
134
|
+
|
|
135
|
+
- Logs no longer contain secret values or other request data. With debug
|
|
136
|
+
logging on (`PIPECAT_CLI_LOG_LEVEL=DEBUG` for the CLI, `PCC_LOG_LEVEL=DEBUG`
|
|
137
|
+
for the Python package), `secrets set` wrote the values it was given to
|
|
138
|
+
stderr, and every API request wrote its body and query there, including
|
|
139
|
+
image pull credentials and session data. Requests now log their method,
|
|
140
|
+
path and how much they carry. Tokens were not in these lines. If you ran
|
|
141
|
+
with debug logging on and the output was kept, for example in CI logs,
|
|
142
|
+
change the values it contains.
|
|
143
|
+
- An unexpected error in the CLI could print its local variables in the
|
|
144
|
+
traceback, including secret values and your token. Tracebacks now show code
|
|
145
|
+
only. To see them as before, set `TYPER_STANDARD_TRACEBACK` to an empty value.
|
|
146
|
+
- A failed build context upload now logs its status and S3 error code, not
|
|
147
|
+
S3's error document, which can repeat signed upload details. This line is
|
|
148
|
+
logged at the default level.
|
|
149
|
+
- `secrets set` no longer shows the first characters of each value when it asks
|
|
150
|
+
you to confirm. It shows a fixed mask, the same for any value. Without
|
|
151
|
+
`--skip` and without a terminal, as in CI, it printed those characters and
|
|
152
|
+
then failed; it now fails first. If such output was kept, change the values
|
|
153
|
+
it contains.
|
|
154
|
+
- The locked versions of `virtualenv`, `packaging`, `distlib`, `Pillow` and
|
|
155
|
+
`setuptools` are upgraded for security fixes. These are the versions in
|
|
156
|
+
`uv.lock`, used to develop and test this package. What it requires when you
|
|
157
|
+
install it has not changed, so upgrade your own environment if it holds an
|
|
158
|
+
older one.
|
|
159
|
+
|
|
8
160
|
## [1.4.0] - 2026-10-22
|
|
9
161
|
|
|
10
162
|
### 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.6.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.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2026, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Warnings about the base image an agent's Dockerfile builds from.
|
|
8
|
+
|
|
9
|
+
``dailyco/pipecat-base:latest`` names no Python, so it carries Python 3.12, and
|
|
10
|
+
it stops building on ``LATEST_RETIRES``. A Dockerfile that takes it is warned
|
|
11
|
+
before its build. Version tags without ``-py3.X``, such as ``0.3.0``, are
|
|
12
|
+
deprecated too, but each is fixed to one image and stays published, so a
|
|
13
|
+
Dockerfile pinned to one keeps building and is not warned.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from rich.markup import escape
|
|
20
|
+
|
|
21
|
+
from pipecatcloud._utils.console_utils import console
|
|
22
|
+
|
|
23
|
+
LATEST_RETIRES = "2027-04-05"
|
|
24
|
+
LATEST_PYTHON = "3.12"
|
|
25
|
+
|
|
26
|
+
# Docker Hub's own names for the repository, which Docker resolves alike.
|
|
27
|
+
_BASE_IMAGE = re.compile(
|
|
28
|
+
r"(?:(?:docker\.io|index\.docker\.io|registry-1\.docker\.io)/)?dailyco/pipecat-base"
|
|
29
|
+
r"(?::(?P<tag>[\w][\w.-]*))?(?P<digest>@\S+)?"
|
|
30
|
+
)
|
|
31
|
+
_FROM = re.compile(r"\s*FROM\s+(?:--\S+\s+)*(?P<image>\S+)", re.IGNORECASE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _instructions(text: str):
|
|
35
|
+
"""Each instruction with the line it starts on, continuation lines joined.
|
|
36
|
+
|
|
37
|
+
Comment lines are dropped, inside a continuation too, as Docker drops them.
|
|
38
|
+
"""
|
|
39
|
+
start, parts = None, []
|
|
40
|
+
for number, line in enumerate(text.splitlines(), start=1):
|
|
41
|
+
if line.lstrip().startswith("#"):
|
|
42
|
+
continue
|
|
43
|
+
if start is None:
|
|
44
|
+
start = number
|
|
45
|
+
if line.rstrip().endswith("\\"):
|
|
46
|
+
parts.append(line.rstrip()[:-1])
|
|
47
|
+
continue
|
|
48
|
+
parts.append(line)
|
|
49
|
+
yield start, " ".join(parts)
|
|
50
|
+
start, parts = None, []
|
|
51
|
+
if parts:
|
|
52
|
+
yield start, " ".join(parts)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def latest_base_images(text: str) -> list[tuple[int, str]]:
|
|
56
|
+
"""The FROM instructions that take ``dailyco/pipecat-base:latest``.
|
|
57
|
+
|
|
58
|
+
Returns each one's line number and image, as written. A FROM with no tag
|
|
59
|
+
takes ``latest`` too. One pinned by digest is fixed, and one built from a
|
|
60
|
+
variable cannot be read here, so neither is returned.
|
|
61
|
+
"""
|
|
62
|
+
found = []
|
|
63
|
+
for number, instruction in _instructions(text):
|
|
64
|
+
match = _FROM.match(instruction)
|
|
65
|
+
if not match:
|
|
66
|
+
continue
|
|
67
|
+
image = match["image"]
|
|
68
|
+
base = _BASE_IMAGE.fullmatch(image)
|
|
69
|
+
if base and not base["digest"] and base["tag"] in (None, "latest"):
|
|
70
|
+
found.append((number, image))
|
|
71
|
+
return found
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def warn_latest_base_image(dockerfile: Path) -> None:
|
|
75
|
+
"""Warn for each FROM in ``dockerfile`` that takes the base image's ``latest``.
|
|
76
|
+
|
|
77
|
+
An unreadable Dockerfile is left to the build to report.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
text = dockerfile.read_text(errors="replace")
|
|
81
|
+
except OSError:
|
|
82
|
+
return
|
|
83
|
+
for number, image in latest_base_images(text):
|
|
84
|
+
console.print(
|
|
85
|
+
f"[yellow]Warning: {escape(dockerfile.name)} line {number} builds FROM "
|
|
86
|
+
f"{escape(image)}, which stops building on {LATEST_RETIRES}. It names no "
|
|
87
|
+
f"Python, and runs Python {LATEST_PYTHON}: to keep it, use "
|
|
88
|
+
f"dailyco/pipecat-base:latest-py{LATEST_PYTHON}.[/yellow]"
|
|
89
|
+
)
|
|
@@ -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
|
|
|
@@ -762,7 +763,7 @@ def report_git_deploy_result(
|
|
|
762
763
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
763
764
|
|
|
764
765
|
check_hint = (
|
|
765
|
-
f"[dim]Check it with [bold]{PIPECAT_CLI_NAME}
|
|
766
|
+
f"[dim]Check it with [bold]{PIPECAT_CLI_NAME} project status {agent_name}[/bold].[/dim]"
|
|
766
767
|
)
|
|
767
768
|
label = "First deploy" if first_deploy else "Deploy"
|
|
768
769
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2026, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Shared helpers for projects and environments (PCC-1255).
|
|
8
|
+
|
|
9
|
+
An environment is an agent of its own, linked to the project it belongs to.
|
|
10
|
+
The project doubles as its own `production` environment, so a project is
|
|
11
|
+
simply an agent with no parent. These helpers read that shape out of the
|
|
12
|
+
service payloads the API already returns, and mirror the name rules it
|
|
13
|
+
enforces so a typo fails before the round trip.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
#: What the project itself is called among its environments.
|
|
19
|
+
PRODUCTION_ENVIRONMENT_NAME = "production"
|
|
20
|
+
|
|
21
|
+
#: PR previews are named `pr-<n>`, so the API reserves the prefix.
|
|
22
|
+
PREVIEW_ENVIRONMENT_PREFIX = "pr-"
|
|
23
|
+
|
|
24
|
+
#: The width of the environments.name column.
|
|
25
|
+
MAX_ENVIRONMENT_NAME_LENGTH = 50
|
|
26
|
+
|
|
27
|
+
# The agent-name rule, which environment names follow too.
|
|
28
|
+
_ENVIRONMENT_NAME_SHAPE = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])$")
|
|
29
|
+
|
|
30
|
+
_PREVIEW_NAME = re.compile(r"^pr-(\d+)$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def environment_name_problem(name: str) -> str | None:
|
|
34
|
+
"""Why the API would refuse this environment name, or None when it's fine."""
|
|
35
|
+
if not name:
|
|
36
|
+
return "Enter a name for the environment."
|
|
37
|
+
if name == PRODUCTION_ENVIRONMENT_NAME:
|
|
38
|
+
return f"'{PRODUCTION_ENVIRONMENT_NAME}' is the project itself, so it can't be an environment name."
|
|
39
|
+
if name.startswith(PREVIEW_ENVIRONMENT_PREFIX):
|
|
40
|
+
return f"Names starting with '{PREVIEW_ENVIRONMENT_PREFIX}' are reserved for PR previews."
|
|
41
|
+
if len(name) > MAX_ENVIRONMENT_NAME_LENGTH:
|
|
42
|
+
return f"Use at most {MAX_ENVIRONMENT_NAME_LENGTH} characters."
|
|
43
|
+
if not _ENVIRONMENT_NAME_SHAPE.match(name):
|
|
44
|
+
return (
|
|
45
|
+
"Use two or more lowercase letters, digits and dashes, starting and "
|
|
46
|
+
"ending with a letter or digit."
|
|
47
|
+
)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_preview_name(name: str | None) -> bool:
|
|
52
|
+
"""Whether an environment's name is a PR preview's."""
|
|
53
|
+
return bool(name and _PREVIEW_NAME.match(name))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def environment_sort_key(name: str) -> tuple[int, int, str]:
|
|
57
|
+
"""Named environments by name, then PR previews, newest pull request first."""
|
|
58
|
+
preview = _PREVIEW_NAME.match(name or "")
|
|
59
|
+
if preview:
|
|
60
|
+
return (1, -int(preview.group(1)), "")
|
|
61
|
+
return (0, 0, name or "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_environment(service: dict) -> bool:
|
|
65
|
+
"""Whether this agent is an environment of another agent."""
|
|
66
|
+
return bool(service.get("parentServiceId"))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def projects_in(services: list[dict]) -> list[dict]:
|
|
70
|
+
"""The agents that are projects: the ones that are not environments."""
|
|
71
|
+
return [service for service in services if not is_environment(service)]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def environments_of(services: list[dict], project: dict) -> list[dict]:
|
|
75
|
+
"""A project's environments, named ones first and then its PR previews."""
|
|
76
|
+
children = [
|
|
77
|
+
service for service in services if service.get("parentServiceId") == project.get("id")
|
|
78
|
+
]
|
|
79
|
+
return sorted(children, key=lambda s: environment_sort_key(s.get("environmentName") or ""))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def environments_with_production(services: list[dict], project: dict) -> list[dict]:
|
|
83
|
+
"""A project's environments, the project itself first as `production`.
|
|
84
|
+
|
|
85
|
+
The project doubles as that environment, so a listing that left it out
|
|
86
|
+
would contradict the model the rest of the group works to. Callers that
|
|
87
|
+
mean only the agents linked to the project use `environments_of`.
|
|
88
|
+
"""
|
|
89
|
+
return [
|
|
90
|
+
{**project, "environmentName": PRODUCTION_ENVIRONMENT_NAME},
|
|
91
|
+
*environments_of(services, project),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def promotion_source(environment: dict) -> dict | None:
|
|
96
|
+
"""What promoting this environment deploys: its build, or the image it runs.
|
|
97
|
+
|
|
98
|
+
None when it has nothing deployed. Promotion moves code only, so this is
|
|
99
|
+
the whole payload the project is updated with.
|
|
100
|
+
"""
|
|
101
|
+
deployment = environment.get("deployment") or {}
|
|
102
|
+
build_id = deployment.get("buildId")
|
|
103
|
+
if build_id:
|
|
104
|
+
return {"buildId": build_id}
|
|
105
|
+
image = ((deployment.get("manifest") or {}).get("spec") or {}).get("image")
|
|
106
|
+
if image:
|
|
107
|
+
return {"image": image}
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def describe_promotion_source(source: dict) -> str:
|
|
112
|
+
"""A promotion source as a short phrase, for confirmations and messages."""
|
|
113
|
+
build_id = source.get("buildId")
|
|
114
|
+
return f"build {build_id}" if build_id else str(source.get("image"))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def previews_on(service: dict) -> bool:
|
|
118
|
+
"""Whether this project has PR previews on.
|
|
119
|
+
|
|
120
|
+
Choosing the secret set previews mount is what turns them on, so the set
|
|
121
|
+
being present is the setting.
|
|
122
|
+
"""
|
|
123
|
+
git = service.get("git") or {}
|
|
124
|
+
return bool(git.get("previewSecretSet"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def project_label(service: dict, project_names: dict[str, str]) -> str:
|
|
128
|
+
"""`project > environment` for an environment, or a dash for a project.
|
|
129
|
+
|
|
130
|
+
`project_names` maps agent id to name, which is how a parent's name is
|
|
131
|
+
resolved: the service payload carries only `parentServiceId`.
|
|
132
|
+
"""
|
|
133
|
+
if not is_environment(service):
|
|
134
|
+
return "—"
|
|
135
|
+
parent = project_names.get(service.get("parentServiceId") or "", "?")
|
|
136
|
+
return f"{parent} > {service.get('environmentName') or '?'}"
|
|
@@ -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
|
+
)
|