pipecatcloud 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pipecatcloud-0.2.4/.github/workflows/publish-pypi.yml +84 -0
- pipecatcloud-0.2.4/.github/workflows/publish-test.yml +73 -0
- pipecatcloud-0.2.4/.github/workflows/tests.yml +41 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/CHANGELOG.md +20 -0
- {pipecatcloud-0.2.2/src/pipecatcloud.egg-info → pipecatcloud-0.2.4}/PKG-INFO +2 -2
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/README.md +1 -1
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/pyproject.toml +14 -1
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/_utils/deploy_utils.py +12 -3
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/agent.py +4 -4
- pipecatcloud-0.2.4/src/pipecatcloud/cli/commands/docker.py +344 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/init.py +8 -8
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/entry_point.py +2 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/config.py +1 -1
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4/src/pipecatcloud.egg-info}/PKG-INFO +2 -2
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud.egg-info/SOURCES.txt +4 -2
- pipecatcloud-0.2.4/tests/test_docker_commands.py +169 -0
- pipecatcloud-0.2.2/.github/workflows/publish-pypi.yml +0 -66
- pipecatcloud-0.2.2/.github/workflows/publish-test.yml +0 -67
- pipecatcloud-0.2.2/dev-requirements.txt +0 -8
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/.github/workflows/python-publish.yml +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/.gitignore +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/LICENSE +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/pytest.ini +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/setup.cfg +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/_utils/local_runner.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/api.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/agent.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/deploy.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/run.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/commands/secrets.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/tests/__init__.py +0 -0
- {pipecatcloud-0.2.2 → pipecatcloud-0.2.4}/tests/test_agent_sessions.py +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: 'Build and upload wheels'
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-tags: true
|
|
20
|
+
fetch-depth: 100
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v3
|
|
24
|
+
with:
|
|
25
|
+
version: 'latest'
|
|
26
|
+
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
run: uv python install 3.12
|
|
29
|
+
|
|
30
|
+
- name: Verify version matches
|
|
31
|
+
run: |
|
|
32
|
+
PROJECT_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
33
|
+
if [ "$PROJECT_VERSION" != "${{ github.event.inputs.version }}" ]; then
|
|
34
|
+
echo "Error: Input version (${{ github.event.inputs.version }}) does not match pyproject.toml version ($PROJECT_VERSION)"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
- name: Install development dependencies
|
|
39
|
+
run: uv sync --group dev
|
|
40
|
+
|
|
41
|
+
- name: Build project
|
|
42
|
+
run: uv build
|
|
43
|
+
|
|
44
|
+
- name: Upload wheels
|
|
45
|
+
uses: actions/upload-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: wheels
|
|
48
|
+
path: ./dist
|
|
49
|
+
|
|
50
|
+
publish-to-pypi:
|
|
51
|
+
name: 'Publish to PyPI'
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
needs: [build]
|
|
54
|
+
environment:
|
|
55
|
+
name: pypi
|
|
56
|
+
url: https://pypi.org/p/pipecat-cloud
|
|
57
|
+
permissions:
|
|
58
|
+
id-token: write
|
|
59
|
+
contents: write # Needed for tagging
|
|
60
|
+
steps:
|
|
61
|
+
- name: Checkout code
|
|
62
|
+
uses: actions/checkout@v4
|
|
63
|
+
with:
|
|
64
|
+
fetch-tags: true
|
|
65
|
+
fetch-depth: 100
|
|
66
|
+
|
|
67
|
+
- name: Create release tag
|
|
68
|
+
run: |
|
|
69
|
+
git config --local user.email "action@github.com"
|
|
70
|
+
git config --local user.name "GitHub Action"
|
|
71
|
+
git tag -a "v${{ github.event.inputs.version }}" -m "Version ${{ github.event.inputs.version }}"
|
|
72
|
+
git push origin "v${{ github.event.inputs.version }}"
|
|
73
|
+
|
|
74
|
+
- name: Download wheels
|
|
75
|
+
uses: actions/download-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: wheels
|
|
78
|
+
path: ./dist
|
|
79
|
+
|
|
80
|
+
- name: Publish to PyPI
|
|
81
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
82
|
+
with:
|
|
83
|
+
verbose: true
|
|
84
|
+
print-hash: true
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Publish to TEST PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: 'Build and upload wheels'
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-tags: true
|
|
22
|
+
fetch-depth: 100
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v3
|
|
26
|
+
with:
|
|
27
|
+
version: 'latest'
|
|
28
|
+
|
|
29
|
+
- name: Set up Python
|
|
30
|
+
run: uv python install 3.12
|
|
31
|
+
|
|
32
|
+
- name: Verify version matches
|
|
33
|
+
run: |
|
|
34
|
+
PROJECT_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
35
|
+
if [ "$PROJECT_VERSION" != "${{ github.event.inputs.version }}" ]; then
|
|
36
|
+
echo "Error: Input version (${{ github.event.inputs.version }}) does not match pyproject.toml version ($PROJECT_VERSION)"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Install development dependencies
|
|
41
|
+
run: uv sync --group dev
|
|
42
|
+
|
|
43
|
+
- name: Build project
|
|
44
|
+
run: uv build
|
|
45
|
+
|
|
46
|
+
- name: Upload wheels
|
|
47
|
+
uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: wheels
|
|
50
|
+
path: ./dist
|
|
51
|
+
|
|
52
|
+
publish-to-test-pypi:
|
|
53
|
+
name: 'Publish to Test PyPI'
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
needs: [build]
|
|
56
|
+
environment:
|
|
57
|
+
name: testpypi
|
|
58
|
+
url: https://test.pypi.org/project/pipecat-cloud
|
|
59
|
+
permissions:
|
|
60
|
+
id-token: write
|
|
61
|
+
steps:
|
|
62
|
+
- name: Download wheels
|
|
63
|
+
uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: wheels
|
|
66
|
+
path: ./dist
|
|
67
|
+
|
|
68
|
+
- name: Publish to TestPyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
repository-url: https://test.pypi.org/legacy/
|
|
72
|
+
verbose: true
|
|
73
|
+
print-hash: true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
pull_request:
|
|
9
|
+
branches:
|
|
10
|
+
- '**'
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: build-test-${{ github.event.pull_request.number || github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: 'Unit Tests'
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout repo
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v3
|
|
26
|
+
with:
|
|
27
|
+
version: 'latest'
|
|
28
|
+
|
|
29
|
+
- name: Set up Python
|
|
30
|
+
run: uv python install 3.12
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
uv sync --group dev
|
|
35
|
+
|
|
36
|
+
- name: Test Docker commands
|
|
37
|
+
run: |
|
|
38
|
+
uv run pytest tests/test_docker_commands.py -v
|
|
39
|
+
|
|
40
|
+
# Note: test_agent_sessions.py is excluded because it requires complex
|
|
41
|
+
# API mocking for CI. These tests can be run locally with proper auth.
|
|
@@ -5,6 +5,26 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.4] - 2025-08-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `docker build-push` command for building, tagging, and pushing Docker
|
|
13
|
+
images. Automatically parses registry information from the `image` field in
|
|
14
|
+
`pcc-deploy.toml` and supports both Docker Hub and custom registries. Includes
|
|
15
|
+
real-time build output and helpful authentication error hints.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `pcc init` now points to https://github.com/pipecat-ai/pipecat-quickstart.
|
|
20
|
+
|
|
21
|
+
## [0.2.3] - 2025-08-19
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed an issue where DailySessionArguments required positional args for
|
|
26
|
+
`handle_sigint`, `handle_sigterm`, and `pipeline_idle_timeout_secs`.
|
|
27
|
+
|
|
8
28
|
## [0.2.2] - 2025-08-18
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Cloud hosting for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
6
|
Project-URL: Website, https://docs.pipecat.daily.co/
|
|
@@ -63,7 +63,7 @@ pcc auth login
|
|
|
63
63
|
|
|
64
64
|
2. Login to your account `pcc auth login`
|
|
65
65
|
|
|
66
|
-
3. (Optional): Clone the
|
|
66
|
+
3. (Optional): Clone the quickstart repo [here](https://github.com/pipecat-ai/pipecat-quickstart)
|
|
67
67
|
|
|
68
68
|
4. Build your agent `docker build --platform linux/arm64 -t your-agent-name .`
|
|
69
69
|
|
|
@@ -40,7 +40,7 @@ pcc auth login
|
|
|
40
40
|
|
|
41
41
|
2. Login to your account `pcc auth login`
|
|
42
42
|
|
|
43
|
-
3. (Optional): Clone the
|
|
43
|
+
3. (Optional): Clone the quickstart repo [here](https://github.com/pipecat-ai/pipecat-quickstart)
|
|
44
44
|
|
|
45
45
|
4. Build your agent `docker build --platform linux/arm64 -t your-agent-name .`
|
|
46
46
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pipecatcloud"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Cloud hosting for Pipecat AI applications"
|
|
10
10
|
license = { text = "BSD 2-Clause License" }
|
|
@@ -25,6 +25,19 @@ dependencies = [
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
pipecat = ["pipecat-ai>=0.0.79"]
|
|
27
27
|
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"build~=1.2.2",
|
|
31
|
+
"grpcio-tools~=1.67.1",
|
|
32
|
+
"pre-commit~=4.2.0",
|
|
33
|
+
"pyright~=1.1.402",
|
|
34
|
+
"pytest~=8.4.1",
|
|
35
|
+
"pytest-asyncio~=1.0.0",
|
|
36
|
+
"ruff~=0.12.1",
|
|
37
|
+
"setuptools~=78.1.1",
|
|
38
|
+
"setuptools_scm~=8.3.1",
|
|
39
|
+
]
|
|
40
|
+
|
|
28
41
|
[project.scripts]
|
|
29
42
|
pcc = "pipecatcloud.__main__:main"
|
|
30
43
|
pipecatcloud = "pipecatcloud.__main__:main"
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
-
import os
|
|
8
7
|
import functools
|
|
9
|
-
|
|
8
|
+
import os
|
|
9
|
+
from typing import Callable, Optional
|
|
10
10
|
|
|
11
11
|
import toml
|
|
12
12
|
from attr import dataclass, field
|
|
@@ -64,6 +64,7 @@ class DeployConfigParams:
|
|
|
64
64
|
secret_set: Optional[str] = None
|
|
65
65
|
scaling: ScalingParams = ScalingParams()
|
|
66
66
|
enable_krisp: bool = False
|
|
67
|
+
docker_config: dict = field(factory=dict)
|
|
67
68
|
|
|
68
69
|
def __attrs_post_init__(self):
|
|
69
70
|
if self.image is not None and ":" not in self.image:
|
|
@@ -77,6 +78,7 @@ class DeployConfigParams:
|
|
|
77
78
|
"secret_set": self.secret_set,
|
|
78
79
|
"scaling": self.scaling.to_dict() if self.scaling else None,
|
|
79
80
|
"enable_krisp": self.enable_krisp,
|
|
81
|
+
"docker_config": self.docker_config,
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
|
|
@@ -97,10 +99,14 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
|
|
|
97
99
|
scaling_data = config_data.pop("scaling", {})
|
|
98
100
|
scaling_params = ScalingParams(**scaling_data)
|
|
99
101
|
|
|
102
|
+
# Extract docker configuration if present
|
|
103
|
+
docker_data = config_data.pop("docker", {})
|
|
104
|
+
|
|
100
105
|
# Create DeployConfigParams with validated data
|
|
101
106
|
validated_config = DeployConfigParams(
|
|
102
107
|
**config_data,
|
|
103
108
|
scaling=scaling_params,
|
|
109
|
+
docker_config=docker_data,
|
|
104
110
|
)
|
|
105
111
|
|
|
106
112
|
# Check for unexpected keys
|
|
@@ -111,6 +117,7 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
|
|
|
111
117
|
"secret_set",
|
|
112
118
|
"scaling",
|
|
113
119
|
"enable_krisp",
|
|
120
|
+
"docker",
|
|
114
121
|
}
|
|
115
122
|
unexpected_keys = set(config_data.keys()) - expected_keys
|
|
116
123
|
if unexpected_keys:
|
|
@@ -128,13 +135,15 @@ def with_deploy_config(func: Callable) -> Callable:
|
|
|
128
135
|
Decorator that loads the deploy config file and injects it into the function.
|
|
129
136
|
If the config file exists, it will be loaded and passed to the function as `deploy_config`.
|
|
130
137
|
"""
|
|
138
|
+
|
|
131
139
|
@functools.wraps(func)
|
|
132
140
|
def wrapper(*args, **kwargs):
|
|
133
141
|
try:
|
|
134
142
|
deploy_config = load_deploy_config_file()
|
|
135
|
-
kwargs[
|
|
143
|
+
kwargs["deploy_config"] = deploy_config
|
|
136
144
|
except Exception as e:
|
|
137
145
|
logger.error(f"Error loading deploy config: {e}")
|
|
138
146
|
raise ConfigFileError(str(e))
|
|
139
147
|
return func(*args, **kwargs)
|
|
148
|
+
|
|
140
149
|
return wrapper
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
import warnings
|
|
8
|
-
from dataclasses import dataclass
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any, Optional
|
|
10
10
|
|
|
11
11
|
from fastapi import WebSocket
|
|
@@ -32,9 +32,9 @@ except ImportError:
|
|
|
32
32
|
will be removed in a future release.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
handle_sigint: bool
|
|
36
|
-
handle_sigterm: bool
|
|
37
|
-
pipeline_idle_timeout_secs: int
|
|
35
|
+
handle_sigint: bool = field(init=False)
|
|
36
|
+
handle_sigterm: bool = field(init=False)
|
|
37
|
+
pipeline_idle_timeout_secs: int = field(init=False)
|
|
38
38
|
|
|
39
39
|
def __post_init__(self):
|
|
40
40
|
self.handle_sigint = False
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
from pipecatcloud._utils.async_utils import synchronizer
|
|
15
|
+
from pipecatcloud._utils.console_utils import console
|
|
16
|
+
from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
|
|
17
|
+
|
|
18
|
+
docker_cli = typer.Typer(
|
|
19
|
+
name="docker", help="Docker build and push utilities", no_args_is_help=True
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RegistryType(str, Enum):
|
|
24
|
+
DOCKERHUB = "dockerhub"
|
|
25
|
+
CUSTOM = "custom"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _provide_error_hints(stderr: str, command: list[str], registry_info: dict = None):
|
|
29
|
+
"""Provide helpful error hints only when Docker's message isn't clear enough."""
|
|
30
|
+
stderr_lower = stderr.lower()
|
|
31
|
+
|
|
32
|
+
# Only add hints for authentication errors where the solution isn't obvious
|
|
33
|
+
if _is_auth_error(stderr_lower) and "push" in " ".join(command):
|
|
34
|
+
_suggest_docker_login(registry_info)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_auth_error(stderr_lower: str) -> bool:
|
|
38
|
+
"""Check if the error is authentication related."""
|
|
39
|
+
auth_keywords = ["unauthorized", "access denied", "denied"]
|
|
40
|
+
return any(keyword in stderr_lower for keyword in auth_keywords)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _suggest_docker_login(registry_info: dict = None):
|
|
44
|
+
"""Suggest the appropriate docker login command."""
|
|
45
|
+
console.print("\n[yellow]💡 You need to authenticate with the registry[/yellow]")
|
|
46
|
+
|
|
47
|
+
if registry_info and registry_info.get("type") == "custom" and registry_info.get("url"):
|
|
48
|
+
console.print(f"[yellow] docker login {registry_info['url']}[/yellow]")
|
|
49
|
+
else:
|
|
50
|
+
console.print("[yellow] docker login[/yellow]")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_docker_command(
|
|
54
|
+
command: list[str], description: str, stream_output: bool = True, registry_info: dict = None
|
|
55
|
+
) -> bool:
|
|
56
|
+
"""Run a docker command and handle output/errors."""
|
|
57
|
+
try:
|
|
58
|
+
console.print(f"[dim]{description}...[/dim]")
|
|
59
|
+
|
|
60
|
+
if stream_output:
|
|
61
|
+
# Stream output in real-time for build commands
|
|
62
|
+
process = subprocess.Popen(
|
|
63
|
+
command,
|
|
64
|
+
stdout=subprocess.PIPE,
|
|
65
|
+
stderr=subprocess.STDOUT,
|
|
66
|
+
text=True,
|
|
67
|
+
bufsize=1,
|
|
68
|
+
universal_newlines=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Stream output line by line
|
|
72
|
+
for line in process.stdout:
|
|
73
|
+
console.print(f"[dim]{line.rstrip()}[/dim]")
|
|
74
|
+
|
|
75
|
+
process.wait()
|
|
76
|
+
if process.returncode != 0:
|
|
77
|
+
raise subprocess.CalledProcessError(process.returncode, command)
|
|
78
|
+
else:
|
|
79
|
+
# Capture output for push commands (less verbose)
|
|
80
|
+
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
|
81
|
+
if result.stdout.strip():
|
|
82
|
+
console.print(f"[dim]{result.stdout.strip()}[/dim]")
|
|
83
|
+
|
|
84
|
+
return True
|
|
85
|
+
except subprocess.CalledProcessError as e:
|
|
86
|
+
console.error(f"Docker command failed: {' '.join(command)}")
|
|
87
|
+
if e.stdout:
|
|
88
|
+
console.print(f"[red]stdout: {e.stdout}[/red]")
|
|
89
|
+
if e.stderr:
|
|
90
|
+
console.print(f"[red]stderr: {e.stderr}[/red]")
|
|
91
|
+
|
|
92
|
+
# Provide helpful hints for common errors
|
|
93
|
+
_provide_error_hints(e.stderr, command, registry_info)
|
|
94
|
+
return False
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
console.error("Docker not found. Please ensure Docker is installed and in your PATH.")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_image_name(
|
|
101
|
+
registry_type: RegistryType, username: str, agent_name: str, registry_url: Optional[str] = None
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Build the full image name based on registry type."""
|
|
104
|
+
if registry_type == RegistryType.DOCKERHUB:
|
|
105
|
+
return f"{username}/{agent_name}"
|
|
106
|
+
elif registry_type == RegistryType.CUSTOM:
|
|
107
|
+
if not registry_url:
|
|
108
|
+
raise ValueError("registry_url is required for custom registries")
|
|
109
|
+
return f"{registry_url}/{username}/{agent_name}"
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError(f"Unsupported registry type: {registry_type}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@docker_cli.command(name="build-push", help="Build, tag, and push Docker image")
|
|
115
|
+
@synchronizer.create_blocking
|
|
116
|
+
@with_deploy_config
|
|
117
|
+
async def build_push(
|
|
118
|
+
deploy_config=typer.Option(None, hidden=True),
|
|
119
|
+
agent_name: str = typer.Argument(
|
|
120
|
+
None, help="Name of the agent to build image for e.g. 'my-agent'", show_default=False
|
|
121
|
+
),
|
|
122
|
+
registry: RegistryType = typer.Option(
|
|
123
|
+
None,
|
|
124
|
+
"--registry",
|
|
125
|
+
"-r",
|
|
126
|
+
help="Registry type to push to",
|
|
127
|
+
rich_help_panel="Registry Configuration",
|
|
128
|
+
),
|
|
129
|
+
registry_username: str = typer.Option(
|
|
130
|
+
None,
|
|
131
|
+
"--username",
|
|
132
|
+
"-u",
|
|
133
|
+
help="Registry username",
|
|
134
|
+
rich_help_panel="Registry Configuration",
|
|
135
|
+
),
|
|
136
|
+
registry_url: str = typer.Option(
|
|
137
|
+
None,
|
|
138
|
+
"--registry-url",
|
|
139
|
+
help="Custom registry URL (required for custom registries)",
|
|
140
|
+
rich_help_panel="Registry Configuration",
|
|
141
|
+
),
|
|
142
|
+
version: str = typer.Option(
|
|
143
|
+
None,
|
|
144
|
+
"--version",
|
|
145
|
+
"-v",
|
|
146
|
+
help="Version tag for the image",
|
|
147
|
+
rich_help_panel="Build Configuration",
|
|
148
|
+
),
|
|
149
|
+
no_push: bool = typer.Option(
|
|
150
|
+
False,
|
|
151
|
+
"--no-push",
|
|
152
|
+
help="Build and tag only, do not push to registry",
|
|
153
|
+
rich_help_panel="Build Configuration",
|
|
154
|
+
),
|
|
155
|
+
no_latest: bool = typer.Option(
|
|
156
|
+
False,
|
|
157
|
+
"--no-latest",
|
|
158
|
+
help="Do not tag as 'latest'",
|
|
159
|
+
rich_help_panel="Build Configuration",
|
|
160
|
+
),
|
|
161
|
+
):
|
|
162
|
+
"""Build, tag, and optionally push a Docker image for your agent."""
|
|
163
|
+
|
|
164
|
+
# Load values from deployment config file (if one exists)
|
|
165
|
+
partial_config = deploy_config or DeployConfigParams()
|
|
166
|
+
docker_config = getattr(partial_config, "docker_config", {})
|
|
167
|
+
|
|
168
|
+
# Parse image field to extract registry info (if present)
|
|
169
|
+
parsed_registry = None
|
|
170
|
+
parsed_username = None
|
|
171
|
+
parsed_agent_name = None
|
|
172
|
+
parsed_version = None
|
|
173
|
+
parsed_registry_url = None
|
|
174
|
+
|
|
175
|
+
if partial_config.image:
|
|
176
|
+
# Parse image like "markatdaily/voice-starter:0.5" or "registry.com/user/app:v1.0"
|
|
177
|
+
image_parts = partial_config.image.split("/")
|
|
178
|
+
|
|
179
|
+
if len(image_parts) == 2:
|
|
180
|
+
# Format: "username/repo:tag" (Docker Hub)
|
|
181
|
+
parsed_registry = "dockerhub"
|
|
182
|
+
parsed_username = image_parts[0]
|
|
183
|
+
repo_and_tag = image_parts[1]
|
|
184
|
+
elif len(image_parts) == 3:
|
|
185
|
+
# Format: "registry.com/username/repo:tag" (Custom registry)
|
|
186
|
+
parsed_registry = "custom"
|
|
187
|
+
parsed_registry_url = image_parts[0]
|
|
188
|
+
parsed_username = image_parts[1]
|
|
189
|
+
repo_and_tag = image_parts[2]
|
|
190
|
+
else:
|
|
191
|
+
# Just "repo:tag" - use for agent name only
|
|
192
|
+
repo_and_tag = partial_config.image
|
|
193
|
+
|
|
194
|
+
# Extract agent name and version from "repo:tag"
|
|
195
|
+
if ":" in repo_and_tag:
|
|
196
|
+
parsed_agent_name, parsed_version = repo_and_tag.split(":", 1)
|
|
197
|
+
else:
|
|
198
|
+
parsed_agent_name = repo_and_tag
|
|
199
|
+
parsed_version = "latest"
|
|
200
|
+
|
|
201
|
+
# Resolve final values with precedence: CLI args > parsed from image > docker config > defaults
|
|
202
|
+
final_agent_name = agent_name or parsed_agent_name or partial_config.agent_name
|
|
203
|
+
final_registry = registry or parsed_registry or docker_config.get("registry")
|
|
204
|
+
final_username = registry_username or parsed_username or docker_config.get("registry_username")
|
|
205
|
+
final_registry_url = registry_url or parsed_registry_url or docker_config.get("registry_url")
|
|
206
|
+
final_version = version or parsed_version or "latest"
|
|
207
|
+
final_no_latest = no_latest or not docker_config.get("auto_latest", True)
|
|
208
|
+
|
|
209
|
+
# Validate required parameters
|
|
210
|
+
if not final_agent_name:
|
|
211
|
+
console.error("Agent name is required. Provide via argument or pcc-deploy.toml")
|
|
212
|
+
return typer.Exit(1)
|
|
213
|
+
|
|
214
|
+
if not no_push:
|
|
215
|
+
if not final_registry:
|
|
216
|
+
console.error(
|
|
217
|
+
"Registry type is required when pushing. Use --registry or configure in pcc-deploy.toml"
|
|
218
|
+
)
|
|
219
|
+
return typer.Exit(1)
|
|
220
|
+
|
|
221
|
+
if not final_username:
|
|
222
|
+
console.error(
|
|
223
|
+
"Registry username is required when pushing. Use --username or configure in pcc-deploy.toml"
|
|
224
|
+
)
|
|
225
|
+
return typer.Exit(1)
|
|
226
|
+
|
|
227
|
+
if final_registry == RegistryType.CUSTOM and not final_registry_url:
|
|
228
|
+
console.error(
|
|
229
|
+
"Registry URL is required for custom registries. Use --registry-url or configure in pcc-deploy.toml"
|
|
230
|
+
)
|
|
231
|
+
return typer.Exit(1)
|
|
232
|
+
|
|
233
|
+
# Build image name
|
|
234
|
+
if no_push:
|
|
235
|
+
# For local builds, just use agent name
|
|
236
|
+
base_image_name = final_agent_name
|
|
237
|
+
else:
|
|
238
|
+
try:
|
|
239
|
+
base_image_name = _build_image_name(
|
|
240
|
+
RegistryType(final_registry), final_username, final_agent_name, final_registry_url
|
|
241
|
+
)
|
|
242
|
+
except ValueError as e:
|
|
243
|
+
console.error(str(e))
|
|
244
|
+
return typer.Exit(1)
|
|
245
|
+
|
|
246
|
+
# Prepare image tags
|
|
247
|
+
version_tag = f"{base_image_name}:{final_version}"
|
|
248
|
+
latest_tag = f"{base_image_name}:latest"
|
|
249
|
+
|
|
250
|
+
# Build Docker command
|
|
251
|
+
build_tags = ["-t", version_tag]
|
|
252
|
+
if not final_no_latest:
|
|
253
|
+
build_tags.extend(["-t", latest_tag])
|
|
254
|
+
|
|
255
|
+
build_command = [
|
|
256
|
+
"docker",
|
|
257
|
+
"buildx",
|
|
258
|
+
"build",
|
|
259
|
+
"--platform=linux/arm64",
|
|
260
|
+
"--load",
|
|
261
|
+
*build_tags,
|
|
262
|
+
".",
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
# Display build configuration
|
|
266
|
+
tags_display = [version_tag]
|
|
267
|
+
if not final_no_latest:
|
|
268
|
+
tags_display.append(latest_tag)
|
|
269
|
+
|
|
270
|
+
console.print(
|
|
271
|
+
Panel(
|
|
272
|
+
f"[bold white]Agent:[/bold white] [green]{final_agent_name}[/green]\n"
|
|
273
|
+
f"[bold white]Platform:[/bold white] [green]linux/arm64[/green]\n"
|
|
274
|
+
f"[bold white]Tags:[/bold white] [green]{', '.join(tags_display)}[/green]\n"
|
|
275
|
+
f"[bold white]Push:[/bold white] {'[red]No[/red]' if no_push else f'[green]Yes ({final_registry})[/green]'}",
|
|
276
|
+
title="Docker Build Configuration",
|
|
277
|
+
title_align="left",
|
|
278
|
+
border_style="yellow",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if not typer.confirm("Do you want to proceed with the build?", default=True):
|
|
283
|
+
console.cancel()
|
|
284
|
+
return typer.Exit()
|
|
285
|
+
|
|
286
|
+
# Execute build
|
|
287
|
+
console.print("\n[bold cyan]Building Docker image...[/bold cyan]")
|
|
288
|
+
if not run_docker_command(build_command, f"Building {final_agent_name}", stream_output=True):
|
|
289
|
+
return typer.Exit(1)
|
|
290
|
+
|
|
291
|
+
console.success(f"Successfully built image(s): {', '.join(tags_display)}")
|
|
292
|
+
|
|
293
|
+
# Push if requested
|
|
294
|
+
if not no_push:
|
|
295
|
+
console.print(f"\n[bold cyan]Pushing to {final_registry}...[/bold cyan]")
|
|
296
|
+
|
|
297
|
+
# Prepare registry info for better error messages
|
|
298
|
+
registry_info = {
|
|
299
|
+
"type": final_registry,
|
|
300
|
+
"url": final_registry_url if final_registry == "custom" else None,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Push version tag
|
|
304
|
+
if not run_docker_command(
|
|
305
|
+
["docker", "push", version_tag],
|
|
306
|
+
f"Pushing {version_tag}",
|
|
307
|
+
stream_output=False,
|
|
308
|
+
registry_info=registry_info,
|
|
309
|
+
):
|
|
310
|
+
return typer.Exit(1)
|
|
311
|
+
|
|
312
|
+
# Push latest tag if created
|
|
313
|
+
if not final_no_latest:
|
|
314
|
+
if not run_docker_command(
|
|
315
|
+
["docker", "push", latest_tag],
|
|
316
|
+
f"Pushing {latest_tag}",
|
|
317
|
+
stream_output=False,
|
|
318
|
+
registry_info=registry_info,
|
|
319
|
+
):
|
|
320
|
+
return typer.Exit(1)
|
|
321
|
+
|
|
322
|
+
pushed_tags = [version_tag]
|
|
323
|
+
if not final_no_latest:
|
|
324
|
+
pushed_tags.append(latest_tag)
|
|
325
|
+
|
|
326
|
+
console.success(f"Successfully pushed: {', '.join(pushed_tags)}")
|
|
327
|
+
else:
|
|
328
|
+
console.print("\n[yellow]Skipping push (--no-push specified)[/yellow]")
|
|
329
|
+
|
|
330
|
+
# Display next steps
|
|
331
|
+
if not no_push:
|
|
332
|
+
console.print("\n[dim]Your image is now available at:[/dim]")
|
|
333
|
+
for tag in tags_display:
|
|
334
|
+
console.print(f" [bold]{tag}[/bold]")
|
|
335
|
+
|
|
336
|
+
console.print("\n[dim]To deploy this image, update your pcc-deploy.toml:[/dim]")
|
|
337
|
+
console.print(f' [bold]image = "{version_tag}"[/bold]')
|
|
338
|
+
console.print("\n[dim]Then run:[/dim]")
|
|
339
|
+
console.print(" [bold]pipecatcloud deploy[/bold]")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def create_docker_command(app: typer.Typer):
|
|
343
|
+
"""Add docker command to the main CLI app."""
|
|
344
|
+
app.add_typer(docker_cli, rich_help_panel="Commands")
|
|
@@ -19,14 +19,14 @@ from pipecatcloud.config import config
|
|
|
19
19
|
# ----- Init
|
|
20
20
|
|
|
21
21
|
FILES_TO_EXTRACT = {
|
|
22
|
-
"pipecat-
|
|
23
|
-
"pipecat-
|
|
24
|
-
"pipecat-
|
|
25
|
-
"pipecat-
|
|
26
|
-
"pipecat-
|
|
27
|
-
"pipecat-
|
|
28
|
-
"pipecat-
|
|
29
|
-
"pipecat-
|
|
22
|
+
"pipecat-quickstart-main/bot.py",
|
|
23
|
+
"pipecat-quickstart-main/Dockerfile",
|
|
24
|
+
"pipecat-quickstart-main/pcc-deploy.toml",
|
|
25
|
+
"pipecat-quickstart-main/README.md",
|
|
26
|
+
"pipecat-quickstart-main/.gitignore",
|
|
27
|
+
"pipecat-quickstart-main/env.example",
|
|
28
|
+
"pipecat-quickstart-main/pyproject.toml",
|
|
29
|
+
"pipecat-quickstart-main/uv.lock",
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
|
|
@@ -13,6 +13,7 @@ from pipecatcloud._utils.console_utils import console
|
|
|
13
13
|
from pipecatcloud.cli.commands.agent import agent_cli
|
|
14
14
|
from pipecatcloud.cli.commands.auth import auth_cli
|
|
15
15
|
from pipecatcloud.cli.commands.deploy import create_deploy_command
|
|
16
|
+
from pipecatcloud.cli.commands.docker import create_docker_command
|
|
16
17
|
from pipecatcloud.cli.commands.init import create_init_command
|
|
17
18
|
from pipecatcloud.cli.commands.organizations import organization_cli
|
|
18
19
|
from pipecatcloud.cli.commands.run import create_run_command
|
|
@@ -80,6 +81,7 @@ def cli(
|
|
|
80
81
|
create_run_command(entrypoint_cli_typer)
|
|
81
82
|
create_init_command(entrypoint_cli_typer)
|
|
82
83
|
create_deploy_command(entrypoint_cli_typer)
|
|
84
|
+
create_docker_command(entrypoint_cli_typer)
|
|
83
85
|
entrypoint_cli_typer.add_typer(auth_cli, rich_help_panel="Commands")
|
|
84
86
|
entrypoint_cli_typer.add_typer(organization_cli, rich_help_panel="Commands")
|
|
85
87
|
entrypoint_cli_typer.add_typer(secrets_cli, rich_help_panel="Commands")
|
|
@@ -19,7 +19,7 @@ _SETTINGS = {
|
|
|
19
19
|
"api_host": _Setting("https://api.pipecat.daily.co"),
|
|
20
20
|
"dashboard_host": _Setting("https://pipecat.daily.co"),
|
|
21
21
|
"init_zip_url": _Setting(
|
|
22
|
-
"https://github.com/
|
|
22
|
+
"https://github.com/pipecat-ai/pipecat-quickstart/archive/refs/heads/main.zip"
|
|
23
23
|
),
|
|
24
24
|
"onboarding_path": _Setting("/v1/onboarding"),
|
|
25
25
|
"login_path": _Setting("/auth/login"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Cloud hosting for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
6
|
Project-URL: Website, https://docs.pipecat.daily.co/
|
|
@@ -63,7 +63,7 @@ pcc auth login
|
|
|
63
63
|
|
|
64
64
|
2. Login to your account `pcc auth login`
|
|
65
65
|
|
|
66
|
-
3. (Optional): Clone the
|
|
66
|
+
3. (Optional): Clone the quickstart repo [here](https://github.com/pipecat-ai/pipecat-quickstart)
|
|
67
67
|
|
|
68
68
|
4. Build your agent `docker build --platform linux/arm64 -t your-agent-name .`
|
|
69
69
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
CHANGELOG.md
|
|
3
3
|
LICENSE
|
|
4
4
|
README.md
|
|
5
|
-
dev-requirements.txt
|
|
6
5
|
pipecat-cloud.png
|
|
7
6
|
pyproject.toml
|
|
8
7
|
pytest.ini
|
|
9
8
|
.github/workflows/publish-pypi.yml
|
|
10
9
|
.github/workflows/publish-test.yml
|
|
11
10
|
.github/workflows/python-publish.yml
|
|
11
|
+
.github/workflows/tests.yml
|
|
12
12
|
src/pipecatcloud/__init__.py
|
|
13
13
|
src/pipecatcloud/__main__.py
|
|
14
14
|
src/pipecatcloud/__version__.py
|
|
@@ -38,9 +38,11 @@ src/pipecatcloud/cli/commands/__init__.py
|
|
|
38
38
|
src/pipecatcloud/cli/commands/agent.py
|
|
39
39
|
src/pipecatcloud/cli/commands/auth.py
|
|
40
40
|
src/pipecatcloud/cli/commands/deploy.py
|
|
41
|
+
src/pipecatcloud/cli/commands/docker.py
|
|
41
42
|
src/pipecatcloud/cli/commands/init.py
|
|
42
43
|
src/pipecatcloud/cli/commands/organizations.py
|
|
43
44
|
src/pipecatcloud/cli/commands/run.py
|
|
44
45
|
src/pipecatcloud/cli/commands/secrets.py
|
|
45
46
|
tests/__init__.py
|
|
46
|
-
tests/test_agent_sessions.py
|
|
47
|
+
tests/test_agent_sessions.py
|
|
48
|
+
tests/test_docker_commands.py
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for Docker build-push command.
|
|
3
|
+
|
|
4
|
+
Tests the core functionality: image parsing, registry detection, and error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from src.pipecatcloud._utils.deploy_utils import DeployConfigParams
|
|
12
|
+
from src.pipecatcloud.cli.commands.docker import (
|
|
13
|
+
RegistryType,
|
|
14
|
+
_build_image_name,
|
|
15
|
+
_is_auth_error,
|
|
16
|
+
_suggest_docker_login,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestImageParsing:
|
|
21
|
+
"""Test parsing of image names for different registry formats."""
|
|
22
|
+
|
|
23
|
+
def test_build_image_name_dockerhub(self):
|
|
24
|
+
"""Test building Docker Hub image names."""
|
|
25
|
+
result = _build_image_name(RegistryType.DOCKERHUB, "my_username", "test-image")
|
|
26
|
+
assert result == "my_username/test-image"
|
|
27
|
+
|
|
28
|
+
def test_build_image_name_custom_registry(self):
|
|
29
|
+
"""Test building custom registry image names."""
|
|
30
|
+
result = _build_image_name(RegistryType.CUSTOM, "myuser", "myapp", "gcr.io")
|
|
31
|
+
assert result == "gcr.io/myuser/myapp"
|
|
32
|
+
|
|
33
|
+
def test_build_image_name_custom_registry_missing_url(self):
|
|
34
|
+
"""Test custom registry without URL raises error."""
|
|
35
|
+
with pytest.raises(ValueError, match="registry_url is required"):
|
|
36
|
+
_build_image_name(RegistryType.CUSTOM, "user", "app")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestConfigParsing:
|
|
40
|
+
"""Test parsing configuration from deploy config."""
|
|
41
|
+
|
|
42
|
+
def test_parse_dockerhub_image(self):
|
|
43
|
+
"""Test parsing Docker Hub format image."""
|
|
44
|
+
# Simulate parsing "my_username/test-image:0.5"
|
|
45
|
+
image = "my_username/test-image:0.5"
|
|
46
|
+
parts = image.split("/")
|
|
47
|
+
|
|
48
|
+
assert len(parts) == 2
|
|
49
|
+
assert parts[0] == "my_username" # username
|
|
50
|
+
|
|
51
|
+
repo_and_tag = parts[1]
|
|
52
|
+
repo, tag = repo_and_tag.split(":", 1)
|
|
53
|
+
assert repo == "test-image"
|
|
54
|
+
assert tag == "0.5"
|
|
55
|
+
|
|
56
|
+
def test_parse_custom_registry_image(self):
|
|
57
|
+
"""Test parsing custom registry format image."""
|
|
58
|
+
# Simulate parsing "gcr.io/my-project/app:v1.0"
|
|
59
|
+
image = "gcr.io/my-project/app:v1.0"
|
|
60
|
+
parts = image.split("/")
|
|
61
|
+
|
|
62
|
+
assert len(parts) == 3
|
|
63
|
+
assert parts[0] == "gcr.io" # registry URL
|
|
64
|
+
assert parts[1] == "my-project" # username/project
|
|
65
|
+
|
|
66
|
+
repo_and_tag = parts[2]
|
|
67
|
+
repo, tag = repo_and_tag.split(":", 1)
|
|
68
|
+
assert repo == "app"
|
|
69
|
+
assert tag == "v1.0"
|
|
70
|
+
|
|
71
|
+
def test_parse_image_no_tag(self):
|
|
72
|
+
"""Test parsing image without explicit tag."""
|
|
73
|
+
image = "my_username/test-image"
|
|
74
|
+
parts = image.split("/")
|
|
75
|
+
|
|
76
|
+
repo_and_tag = parts[1]
|
|
77
|
+
if ":" in repo_and_tag:
|
|
78
|
+
repo, tag = repo_and_tag.split(":", 1)
|
|
79
|
+
else:
|
|
80
|
+
repo = repo_and_tag
|
|
81
|
+
tag = "latest"
|
|
82
|
+
|
|
83
|
+
assert repo == "test-image"
|
|
84
|
+
assert tag == "latest"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestErrorHandling:
|
|
88
|
+
"""Test error detection and hint generation."""
|
|
89
|
+
|
|
90
|
+
def test_is_auth_error_unauthorized(self):
|
|
91
|
+
"""Test detection of unauthorized error."""
|
|
92
|
+
stderr = "Error: unauthorized: authentication required"
|
|
93
|
+
assert _is_auth_error(stderr.lower()) is True
|
|
94
|
+
|
|
95
|
+
def test_is_auth_error_access_denied(self):
|
|
96
|
+
"""Test detection of access denied error."""
|
|
97
|
+
stderr = "Error: access denied to repository"
|
|
98
|
+
assert _is_auth_error(stderr.lower()) is True
|
|
99
|
+
|
|
100
|
+
def test_is_auth_error_denied(self):
|
|
101
|
+
"""Test detection of denied error."""
|
|
102
|
+
stderr = "denied: requested access to the resource is denied"
|
|
103
|
+
assert _is_auth_error(stderr.lower()) is True
|
|
104
|
+
|
|
105
|
+
def test_is_auth_error_false(self):
|
|
106
|
+
"""Test non-auth errors are not detected as auth errors."""
|
|
107
|
+
stderr = "Error: failed to build: syntax error in Dockerfile"
|
|
108
|
+
assert _is_auth_error(stderr.lower()) is False
|
|
109
|
+
|
|
110
|
+
@patch("src.pipecatcloud.cli.commands.docker.console")
|
|
111
|
+
def test_suggest_docker_login_dockerhub(self, mock_console):
|
|
112
|
+
"""Test Docker Hub login suggestion."""
|
|
113
|
+
registry_info = {"type": "dockerhub"}
|
|
114
|
+
_suggest_docker_login(registry_info)
|
|
115
|
+
|
|
116
|
+
mock_console.print.assert_any_call(
|
|
117
|
+
"\n[yellow]💡 You need to authenticate with the registry[/yellow]"
|
|
118
|
+
)
|
|
119
|
+
mock_console.print.assert_any_call("[yellow] docker login[/yellow]")
|
|
120
|
+
|
|
121
|
+
@patch("src.pipecatcloud.cli.commands.docker.console")
|
|
122
|
+
def test_suggest_docker_login_custom_registry(self, mock_console):
|
|
123
|
+
"""Test custom registry login suggestion."""
|
|
124
|
+
registry_info = {"type": "custom", "url": "gcr.io"}
|
|
125
|
+
_suggest_docker_login(registry_info)
|
|
126
|
+
|
|
127
|
+
mock_console.print.assert_any_call(
|
|
128
|
+
"\n[yellow]💡 You need to authenticate with the registry[/yellow]"
|
|
129
|
+
)
|
|
130
|
+
mock_console.print.assert_any_call("[yellow] docker login gcr.io[/yellow]")
|
|
131
|
+
|
|
132
|
+
@patch("src.pipecatcloud.cli.commands.docker.console")
|
|
133
|
+
def test_suggest_docker_login_no_registry_info(self, mock_console):
|
|
134
|
+
"""Test login suggestion with no registry info."""
|
|
135
|
+
_suggest_docker_login(None)
|
|
136
|
+
|
|
137
|
+
mock_console.print.assert_any_call(
|
|
138
|
+
"\n[yellow]💡 You need to authenticate with the registry[/yellow]"
|
|
139
|
+
)
|
|
140
|
+
mock_console.print.assert_any_call("[yellow] docker login[/yellow]")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestDeployConfigIntegration:
|
|
144
|
+
"""Test integration with deploy config system."""
|
|
145
|
+
|
|
146
|
+
def test_deploy_config_with_docker_section(self):
|
|
147
|
+
"""Test deploy config parses docker section correctly."""
|
|
148
|
+
config = DeployConfigParams(
|
|
149
|
+
agent_name="test-agent",
|
|
150
|
+
image="my_username/test:1.0",
|
|
151
|
+
docker_config={
|
|
152
|
+
"registry": "dockerhub",
|
|
153
|
+
"registry_username": "my_username",
|
|
154
|
+
"auto_latest": False,
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
assert config.agent_name == "test-agent"
|
|
159
|
+
assert config.image == "my_username/test:1.0"
|
|
160
|
+
assert config.docker_config["registry"] == "dockerhub"
|
|
161
|
+
assert config.docker_config["auto_latest"] is False
|
|
162
|
+
|
|
163
|
+
def test_deploy_config_minimal(self):
|
|
164
|
+
"""Test minimal deploy config works."""
|
|
165
|
+
config = DeployConfigParams(agent_name="test-agent", image="my_username/test:1.0")
|
|
166
|
+
|
|
167
|
+
assert config.agent_name == "test-agent"
|
|
168
|
+
assert config.image == "my_username/test:1.0"
|
|
169
|
+
assert config.docker_config == {} # Empty dict by default
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
name: Publish to PyPI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
version:
|
|
7
|
-
description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
|
|
8
|
-
required: true
|
|
9
|
-
type: string
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build-and-publish:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
environment:
|
|
15
|
-
name: pypi
|
|
16
|
-
url: https://pypi.org/p/pipecat-cloud
|
|
17
|
-
permissions:
|
|
18
|
-
id-token: write # IMPORTANT: this permission is needed for trusted publishing
|
|
19
|
-
contents: write # Needed for tagging
|
|
20
|
-
|
|
21
|
-
steps:
|
|
22
|
-
- name: Checkout code
|
|
23
|
-
uses: actions/checkout@v4
|
|
24
|
-
|
|
25
|
-
- name: Set up Python
|
|
26
|
-
uses: actions/setup-python@v4
|
|
27
|
-
with:
|
|
28
|
-
python-version: '3.10'
|
|
29
|
-
|
|
30
|
-
- name: Verify version matches
|
|
31
|
-
run: |
|
|
32
|
-
pip install tomli
|
|
33
|
-
PROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
34
|
-
if [ "$PROJECT_VERSION" != "${{ github.event.inputs.version }}" ]; then
|
|
35
|
-
echo "Error: Input version (${{ github.event.inputs.version }}) does not match pyproject.toml version ($PROJECT_VERSION)"
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
- name: Install dependencies
|
|
40
|
-
run: |
|
|
41
|
-
python -m pip install --upgrade pip
|
|
42
|
-
pip install -r dev-requirements.txt
|
|
43
|
-
pip install build twine
|
|
44
|
-
|
|
45
|
-
- name: Build package
|
|
46
|
-
run: python -m build
|
|
47
|
-
|
|
48
|
-
- name: Check and create release tag if needed
|
|
49
|
-
run: |
|
|
50
|
-
git config --local user.email "action@github.com"
|
|
51
|
-
git config --local user.name "GitHub Action"
|
|
52
|
-
|
|
53
|
-
# Check if tag already exists remotely
|
|
54
|
-
if git ls-remote --tags origin | grep -q "refs/tags/v${{ github.event.inputs.version }}"; then
|
|
55
|
-
echo "Tag v${{ github.event.inputs.version }} already exists, skipping tag creation"
|
|
56
|
-
else
|
|
57
|
-
echo "Creating new tag v${{ github.event.inputs.version }}"
|
|
58
|
-
git tag -a "v${{ github.event.inputs.version }}" -m "Version ${{ github.event.inputs.version }}"
|
|
59
|
-
git push origin "v${{ github.event.inputs.version }}"
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
- name: Publish to PyPI
|
|
63
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
64
|
-
with:
|
|
65
|
-
verbose: true
|
|
66
|
-
print-hash: true
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
name: Publish to TEST PyPI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
version:
|
|
7
|
-
description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
|
|
8
|
-
required: true
|
|
9
|
-
type: string
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build-and-publish:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
environment:
|
|
15
|
-
name: pypi
|
|
16
|
-
url: https://test.pypi.org/project/pipecat-cloud
|
|
17
|
-
permissions:
|
|
18
|
-
id-token: write
|
|
19
|
-
contents: write
|
|
20
|
-
|
|
21
|
-
steps:
|
|
22
|
-
- name: Checkout code
|
|
23
|
-
uses: actions/checkout@v4
|
|
24
|
-
|
|
25
|
-
- name: Set up Python
|
|
26
|
-
uses: actions/setup-python@v4
|
|
27
|
-
with:
|
|
28
|
-
python-version: '3.10'
|
|
29
|
-
|
|
30
|
-
- name: Verify version matches
|
|
31
|
-
run: |
|
|
32
|
-
pip install tomli
|
|
33
|
-
PROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
34
|
-
if [ "$PROJECT_VERSION" != "${{ github.event.inputs.version }}" ]; then
|
|
35
|
-
echo "Error: Input version (${{ github.event.inputs.version }}) does not match pyproject.toml version ($PROJECT_VERSION)"
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
- name: Install dependencies
|
|
40
|
-
run: |
|
|
41
|
-
python -m pip install --upgrade pip
|
|
42
|
-
pip install -r dev-requirements.txt
|
|
43
|
-
pip install build twine
|
|
44
|
-
|
|
45
|
-
- name: Build package
|
|
46
|
-
run: python -m build
|
|
47
|
-
|
|
48
|
-
- name: Check and create release tag if needed
|
|
49
|
-
run: |
|
|
50
|
-
git config --local user.email "action@github.com"
|
|
51
|
-
git config --local user.name "GitHub Action"
|
|
52
|
-
|
|
53
|
-
# Check if tag already exists remotely
|
|
54
|
-
if git ls-remote --tags origin | grep -q "refs/tags/v${{ github.event.inputs.version }}"; then
|
|
55
|
-
echo "Tag v${{ github.event.inputs.version }} already exists, skipping tag creation"
|
|
56
|
-
else
|
|
57
|
-
echo "Creating new tag v${{ github.event.inputs.version }}"
|
|
58
|
-
git tag -a "v${{ github.event.inputs.version }}" -m "Version ${{ github.event.inputs.version }}"
|
|
59
|
-
git push origin "v${{ github.event.inputs.version }}"
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
- name: Publish to PyPI
|
|
63
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
64
|
-
with:
|
|
65
|
-
repository-url: https://test.pypi.org/legacy/
|
|
66
|
-
verbose: true
|
|
67
|
-
print-hash: true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|