pipecatcloud 0.2.4__tar.gz → 0.2.5__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.
Files changed (53) hide show
  1. pipecatcloud-0.2.4/.github/workflows/publish-test.yml → pipecatcloud-0.2.5/.github/workflows/publish-pypi.yml +30 -25
  2. pipecatcloud-0.2.5/.github/workflows/publish-test.yml +51 -0
  3. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/CHANGELOG.md +17 -0
  4. {pipecatcloud-0.2.4/src/pipecatcloud.egg-info → pipecatcloud-0.2.5}/PKG-INFO +1 -1
  5. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/pyproject.toml +5 -1
  6. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/__init__.py +4 -0
  7. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/_utils/deploy_utils.py +3 -0
  8. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/agent.py +26 -0
  9. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/api.py +1 -0
  10. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/agent.py +13 -1
  11. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/deploy.py +8 -0
  12. pipecatcloud-0.2.5/src/pipecatcloud/smallwebrtc/session_manager.py +87 -0
  13. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5/src/pipecatcloud.egg-info}/PKG-INFO +1 -1
  14. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud.egg-info/SOURCES.txt +3 -2
  15. pipecatcloud-0.2.5/tests/test_managed_keys.py +437 -0
  16. pipecatcloud-0.2.4/.github/workflows/publish-pypi.yml +0 -84
  17. pipecatcloud-0.2.4/.github/workflows/python-publish.yml +0 -40
  18. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/.github/workflows/tests.yml +0 -0
  19. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/.gitignore +0 -0
  20. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/LICENSE +0 -0
  21. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/README.md +0 -0
  22. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/pipecat-cloud.png +0 -0
  23. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/pytest.ini +0 -0
  24. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/setup.cfg +0 -0
  25. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/__main__.py +0 -0
  26. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/__version__.py +0 -0
  27. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/_utils/__init__.py +0 -0
  28. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/_utils/async_utils.py +0 -0
  29. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  30. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/_utils/console_utils.py +0 -0
  31. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/_utils/local_runner.py +0 -0
  32. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/__init__.py +0 -0
  33. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/api.py +0 -0
  34. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  35. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/auth.py +0 -0
  36. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/docker.py +0 -0
  37. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/init.py +0 -0
  38. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  39. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/run.py +0 -0
  40. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/commands/secrets.py +0 -0
  41. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/config.py +0 -0
  42. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/cli/entry_point.py +0 -0
  43. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/config.py +0 -0
  44. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/exception.py +0 -0
  45. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/py.typed +0 -0
  46. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud/session.py +0 -0
  47. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  48. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  49. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud.egg-info/requires.txt +0 -0
  50. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  51. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/tests/__init__.py +0 -0
  52. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/tests/test_agent_sessions.py +0 -0
  53. {pipecatcloud-0.2.4 → pipecatcloud-0.2.5}/tests/test_docker_commands.py +0 -0
@@ -1,61 +1,67 @@
1
- name: Publish to TEST PyPI
1
+ name: Publish to PyPI
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
- version:
7
- description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
8
- required: true
6
+ gitref:
9
7
  type: string
8
+ description: 'what git tag to build (e.g. v0.0.74)'
9
+ required: true
10
10
 
11
11
  jobs:
12
12
  build:
13
13
  name: 'Build and upload wheels'
14
14
  runs-on: ubuntu-latest
15
- permissions:
16
- contents: write
17
15
  steps:
18
- - name: Checkout code
16
+ - name: Checkout repo
19
17
  uses: actions/checkout@v4
20
18
  with:
21
- fetch-tags: true
22
- fetch-depth: 100
19
+ ref: ${{ github.event.inputs.gitref }}
23
20
 
24
21
  - name: Install uv
25
22
  uses: astral-sh/setup-uv@v3
26
23
  with:
27
24
  version: 'latest'
28
-
29
25
  - name: Set up Python
30
26
  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
27
  - name: Install development dependencies
41
28
  run: uv sync --group dev
42
-
43
29
  - name: Build project
44
30
  run: uv build
45
-
46
31
  - name: Upload wheels
47
32
  uses: actions/upload-artifact@v4
48
33
  with:
49
34
  name: wheels
50
35
  path: ./dist
51
36
 
37
+ publish-to-pypi:
38
+ name: 'Publish to PyPI'
39
+ runs-on: ubuntu-latest
40
+ needs: [build]
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/p/pipecatcloud
44
+ permissions:
45
+ id-token: write
46
+ steps:
47
+ - name: Download wheels
48
+ uses: actions/download-artifact@v4
49
+ with:
50
+ name: wheels
51
+ path: ./dist
52
+ - name: Publish to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
54
+ with:
55
+ verbose: true
56
+ print-hash: true
57
+
52
58
  publish-to-test-pypi:
53
59
  name: 'Publish to Test PyPI'
54
60
  runs-on: ubuntu-latest
55
61
  needs: [build]
56
62
  environment:
57
63
  name: testpypi
58
- url: https://test.pypi.org/project/pipecat-cloud
64
+ url: https://test.pypi.org/p/pipecatcloud
59
65
  permissions:
60
66
  id-token: write
61
67
  steps:
@@ -64,10 +70,9 @@ jobs:
64
70
  with:
65
71
  name: wheels
66
72
  path: ./dist
67
-
68
- - name: Publish to TestPyPI
73
+ - name: Publish to Test PyPI
69
74
  uses: pypa/gh-action-pypi-publish@release/v1
70
75
  with:
71
- repository-url: https://test.pypi.org/legacy/
72
76
  verbose: true
73
77
  print-hash: true
78
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,51 @@
1
+ name: Publish to TEST PyPI
2
+
3
+ on: workflow_dispatch
4
+
5
+ jobs:
6
+ build:
7
+ name: 'Build and upload wheels'
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - name: Checkout repo
11
+ uses: actions/checkout@v4
12
+ with:
13
+ fetch-tags: true
14
+ fetch-depth: 100
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v3
17
+ with:
18
+ version: 'latest'
19
+ - name: Set up Python
20
+ run: uv python install 3.12
21
+ - name: Install development dependencies
22
+ run: uv sync --group dev
23
+ - name: Build project
24
+ run: uv build
25
+ - name: Upload wheels
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: wheels
29
+ path: ./dist
30
+
31
+ publish-to-test-pypi:
32
+ name: 'Publish to Test PyPI'
33
+ runs-on: ubuntu-latest
34
+ needs: [build]
35
+ environment:
36
+ name: testpypi
37
+ url: https://test.pypi.org/p/pipecatcloud
38
+ permissions:
39
+ id-token: write
40
+ steps:
41
+ - name: Download wheels
42
+ uses: actions/download-artifact@v4
43
+ with:
44
+ name: wheels
45
+ path: ./dist
46
+ - name: Publish to Test PyPI
47
+ uses: pypa/gh-action-pypi-publish@release/v1
48
+ with:
49
+ verbose: true
50
+ print-hash: true
51
+ repository-url: https://test.pypi.org/legacy/
@@ -5,6 +5,23 @@ 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.5] - 2025-10-02
9
+
10
+ ### Added
11
+
12
+ - Added a `body` parameter to the fallback type, `WebsocketRunnerArguments`.
13
+
14
+ - Added `SmallWebRTCSessionManager`, a Session Manager to simplify bot creation
15
+ in Pipecat Cloud using `SmallWebRTCTransport`.
16
+ - Added a `SmallWebRTCSessionArguments` dataclass.
17
+
18
+ - `deploy` command now accepts a `--enable-managed-keys` flag which enables
19
+ properly configured agents to use Daily's API keys for supported upstream
20
+ services. This can also be enabled via `pcc-deploy.toml` by adding the
21
+ `enable_managed_keys` attribute and a boolean value.
22
+
23
+ - `agent status` now reflects the state of the managed keys feature.
24
+
8
25
  ## [0.2.4] - 2025-08-26
9
26
 
10
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.4
3
+ Version: 0.2.5
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/
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pipecatcloud"
7
- version = "0.2.4"
7
+ dynamic = ["version"]
8
8
  requires-python = ">=3.10"
9
9
  description = "Cloud hosting for Pipecat AI applications"
10
10
  license = { text = "BSD 2-Clause License" }
@@ -58,3 +58,7 @@ line-length = 100
58
58
 
59
59
  [project.urls]
60
60
  Website = "https://docs.pipecat.daily.co/"
61
+
62
+ [tool.setuptools_scm]
63
+ local_scheme = "no-local-version"
64
+ fallback_version = "0.0.0-dev"
@@ -13,6 +13,7 @@ from pipecatcloud.agent import (
13
13
  DailySessionArguments,
14
14
  PipecatSessionArguments,
15
15
  SessionArguments,
16
+ SmallWebRTCRunnerArguments,
16
17
  WebSocketSessionArguments,
17
18
  )
18
19
  from pipecatcloud.exception import (
@@ -25,6 +26,7 @@ from pipecatcloud.exception import (
25
26
  InvalidError,
26
27
  )
27
28
  from pipecatcloud.session import Session, SessionParams
29
+ from pipecatcloud.smallwebrtc.session_manager import SmallWebRTCSessionManager
28
30
 
29
31
  logger.remove()
30
32
  logger.add(sys.stderr, level=str(os.getenv("PCC_LOG_LEVEL", "INFO")))
@@ -35,10 +37,12 @@ __all__ = [
35
37
  "DailySessionArguments",
36
38
  "PipecatSessionArguments",
37
39
  "SessionArguments",
40
+ "SmallWebRTCRunnerArguments",
38
41
  "WebSocketSessionArguments",
39
42
  # Session classes
40
43
  "Session",
41
44
  "SessionParams",
45
+ "SmallWebRTCSessionManager",
42
46
  # Exception classes
43
47
  "AgentNotHealthyError",
44
48
  "AgentStartError",
@@ -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
+ enable_managed_keys: bool = False
67
68
  docker_config: dict = field(factory=dict)
68
69
 
69
70
  def __attrs_post_init__(self):
@@ -78,6 +79,7 @@ class DeployConfigParams:
78
79
  "secret_set": self.secret_set,
79
80
  "scaling": self.scaling.to_dict() if self.scaling else None,
80
81
  "enable_krisp": self.enable_krisp,
82
+ "enable_managed_keys": self.enable_managed_keys,
81
83
  "docker_config": self.docker_config,
82
84
  }
83
85
 
@@ -117,6 +119,7 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
117
119
  "secret_set",
118
120
  "scaling",
119
121
  "enable_krisp",
122
+ "enable_managed_keys",
120
123
  "docker",
121
124
  }
122
125
  unexpected_keys = set(config_data.keys()) - expected_keys
@@ -16,6 +16,7 @@ try:
16
16
  DailyRunnerArguments,
17
17
  RunnerArguments,
18
18
  WebSocketRunnerArguments,
19
+ SmallWebRTCRunnerArguments,
19
20
  )
20
21
 
21
22
  _PIPECAT_RUNNER_TYPES_AVAILABLE = True
@@ -62,6 +63,17 @@ except ImportError:
62
63
  """
63
64
 
64
65
  websocket: WebSocket
66
+ body: Any
67
+
68
+ @dataclass
69
+ class SmallWebRTCRunnerArguments(RunnerArguments):
70
+ """Fallback Small WebRTC transport session arguments for the runner.
71
+
72
+ Parameters:
73
+ webrtc_connection: Pre-configured WebRTC peer connection
74
+ """
75
+
76
+ webrtc_connection: Any
65
77
 
66
78
 
67
79
  def _warn_standalone_usage():
@@ -131,3 +143,17 @@ class WebSocketSessionArguments(WebSocketRunnerArguments, SessionArguments):
131
143
  def __post_init__(self):
132
144
  WebSocketRunnerArguments.__post_init__(self)
133
145
  _warn_standalone_usage()
146
+
147
+ @dataclass
148
+ class SmallWebRTCSessionArguments(SmallWebRTCRunnerArguments, SessionArguments):
149
+ """SmallWebRTCTransport based agent session arguments.
150
+
151
+ Inherits from SmallWebRTCRunnerArguments for compatibility with pipecat-ai runner.
152
+ When pipecat-ai is not installed, uses a fallback implementation (deprecated).
153
+
154
+ For best compatibility, install: pip install pipecatcloud[pipecat]
155
+ """
156
+
157
+ def __post_init__(self):
158
+ SmallWebRTCRunnerArguments.__post_init__(self)
159
+ _warn_standalone_usage()
@@ -321,6 +321,7 @@ class _API:
321
321
  "maxAgents": deploy_config.scaling.max_agents,
322
322
  },
323
323
  "enableKrisp": deploy_config.enable_krisp,
324
+ "enableIntegratedKeysProxy": deploy_config.enable_managed_keys, # API expects this field name
324
325
  }
325
326
 
326
327
  # Remove None values recursively
@@ -5,7 +5,6 @@
5
5
  #
6
6
 
7
7
  import json
8
- import statistics
9
8
  from enum import Enum
10
9
 
11
10
  import aiohttp
@@ -132,6 +131,19 @@ async def status(
132
131
  "[bold]Updated At:[/bold]",
133
132
  str(data.get("updatedAt", "N/A")),
134
133
  )
134
+
135
+ # Check for Managed Keys status
136
+ # API returns integratedKeysProxy but we display as "Managed Keys"
137
+ integrated_keys = data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("integratedKeysProxy", {})
138
+ if isinstance(integrated_keys, dict):
139
+ integrated_keys_enabled = integrated_keys.get("enabled", False)
140
+ else:
141
+ integrated_keys_enabled = bool(integrated_keys)
142
+
143
+ deployment_table.add_row(
144
+ "[bold]Managed Keys:[/bold]",
145
+ "[green]Enabled[/green]" if integrated_keys_enabled else "[dim]Disabled[/dim]",
146
+ )
135
147
 
136
148
  # Autoscaling info
137
149
  autoscaling_data = data.get("autoScaling", None)
@@ -279,6 +279,12 @@ def create_deploy_command(app: typer.Typer):
279
279
  help="Enable Krisp integration for this deployment",
280
280
  rich_help_panel="Deployment Configuration",
281
281
  ),
282
+ managed_keys: bool = typer.Option(
283
+ False,
284
+ "--enable-managed-keys",
285
+ help="Enable Managed Keys for this deployment",
286
+ rich_help_panel="Deployment Configuration",
287
+ ),
282
288
  skip_confirm: bool = typer.Option(
283
289
  False,
284
290
  "--force",
@@ -345,6 +351,7 @@ def create_deploy_command(app: typer.Typer):
345
351
  else partial_config.scaling.max_agents,
346
352
  )
347
353
  partial_config.enable_krisp = krisp or partial_config.enable_krisp
354
+ partial_config.enable_managed_keys = managed_keys or partial_config.enable_managed_keys
348
355
 
349
356
  # Assert agent name and image are provided
350
357
  if not partial_config.agent_name:
@@ -382,6 +389,7 @@ def create_deploy_command(app: typer.Typer):
382
389
  (f"[bold white]Secret set:[/bold white] {'[dim]None[/dim]' if not partial_config.secret_set else '[green] '+ partial_config.secret_set + '[/green]'}"),
383
390
  (f"[bold white]Image pull secret:[/bold white] {'[dim]None[/dim]' if not partial_config.image_credentials else '[green]' + partial_config.image_credentials + '[/green]'}"),
384
391
  (f"[bold white]Krisp:[/bold white] {'[dim]Disabled[/dim]' if not partial_config.enable_krisp else '[green]Enabled[/green]'}"),
392
+ (f"[bold white]Managed Keys:[/bold white] {'[dim]Disabled[/dim]' if not partial_config.enable_managed_keys else '[green]Enabled[/green]'}"),
385
393
  "\n[dim]Scaling configuration:[/dim]",
386
394
  table,
387
395
  *
@@ -0,0 +1,87 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+
5
+ class SmallWebRTCSessionManager:
6
+ """
7
+ Manages the waiting logic between PipecatSession and SmallWebRTCSession.
8
+
9
+ This class provides:
10
+ - A mechanism to wait for a WebRTC connection within a timeout period.
11
+ - Automatic cancellation if no connection is established in time.
12
+ - A way to complete the waiting future once the session has successfully started.
13
+ """
14
+
15
+ def __init__(self, timeout_seconds: int = 120):
16
+ self._pending_future: Optional[asyncio.Future] = None
17
+ self._timeout_task: Optional[asyncio.Task] = None
18
+ self._timeout_seconds = timeout_seconds
19
+
20
+ async def wait_for_webrtc(self) -> None:
21
+ """
22
+ Creates and waits for a future to be completed when WebRTC connection arrives.
23
+
24
+ Raises:
25
+ TimeoutError: If WebRTC connection is not received within the timeout period
26
+ RuntimeError: If another waiting operation is already in progress
27
+ """
28
+ if self._pending_future is not None:
29
+ raise RuntimeError("Already waiting for WebRTC connection")
30
+
31
+ self._pending_future = asyncio.Future()
32
+
33
+ async def timeout_handler():
34
+ await asyncio.sleep(self._timeout_seconds)
35
+ if self._pending_future and not self._pending_future.done():
36
+ self._pending_future.set_exception(
37
+ TimeoutError(f"WebRTC connection not received within {self._timeout_seconds} seconds")
38
+ )
39
+
40
+ # Create and store the timeout task
41
+ self._timeout_task = asyncio.create_task(timeout_handler())
42
+
43
+ try:
44
+ await self._pending_future
45
+ finally:
46
+ self._cleanup()
47
+
48
+ def complete_session(self) -> bool:
49
+ """
50
+ Completes the waiting future.
51
+
52
+ Returns:
53
+ bool: True if the future was found and completed, False otherwise
54
+ """
55
+ if self._pending_future and not self._pending_future.done():
56
+ # Cancel the timeout task first
57
+ self.cancel_timeout()
58
+ # Complete the future
59
+ self._pending_future.set_result(True)
60
+ return True
61
+ return False
62
+
63
+ def cancel_timeout(self) -> bool:
64
+ """
65
+ Cancels the timeout handler without completing the future.
66
+
67
+ Returns:
68
+ bool: True if the timeout task was found and cancelled, False otherwise
69
+ """
70
+ if self._timeout_task and not self._timeout_task.done():
71
+ self._timeout_task.cancel()
72
+ return True
73
+ return False
74
+
75
+ def _cleanup(self) -> None:
76
+ """Cleans up all resources."""
77
+ self._pending_future = None
78
+ self._timeout_task = None
79
+
80
+ def is_waiting(self) -> bool:
81
+ """
82
+ Checks if currently waiting for WebRTC connection.
83
+
84
+ Returns:
85
+ bool: True if waiting for connection, False otherwise
86
+ """
87
+ return self._pending_future is not None and not self._pending_future.done()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.4
3
+ Version: 0.2.5
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/
@@ -7,7 +7,6 @@ pyproject.toml
7
7
  pytest.ini
8
8
  .github/workflows/publish-pypi.yml
9
9
  .github/workflows/publish-test.yml
10
- .github/workflows/python-publish.yml
11
10
  .github/workflows/tests.yml
12
11
  src/pipecatcloud/__init__.py
13
12
  src/pipecatcloud/__main__.py
@@ -43,6 +42,8 @@ src/pipecatcloud/cli/commands/init.py
43
42
  src/pipecatcloud/cli/commands/organizations.py
44
43
  src/pipecatcloud/cli/commands/run.py
45
44
  src/pipecatcloud/cli/commands/secrets.py
45
+ src/pipecatcloud/smallwebrtc/session_manager.py
46
46
  tests/__init__.py
47
47
  tests/test_agent_sessions.py
48
- tests/test_docker_commands.py
48
+ tests/test_docker_commands.py
49
+ tests/test_managed_keys.py
@@ -0,0 +1,437 @@
1
+ """
2
+ Unit tests for managed keys feature.
3
+
4
+ Tests follow AAA pattern and focus on behaviors/outcomes rather than implementation details.
5
+ Covers data model, TOML parsing, CLI commands, and API integration.
6
+ """
7
+
8
+ import json
9
+ import tempfile
10
+ from pathlib import Path
11
+ from unittest.mock import AsyncMock, MagicMock, patch
12
+
13
+ import pytest
14
+ import toml
15
+
16
+ from src.pipecatcloud._utils.deploy_utils import DeployConfigParams, load_deploy_config_file
17
+ from src.pipecatcloud.api import _API
18
+ from src.pipecatcloud.exception import ConfigFileError
19
+
20
+
21
+ class TestDeployConfigDataModel:
22
+ """Test the DeployConfigParams data model with managed keys support."""
23
+
24
+ def test_default_keys_is_disabled(self):
25
+ """When not specified, managed keys should default to disabled."""
26
+ # Arrange & Act
27
+ config = DeployConfigParams()
28
+
29
+ # Assert
30
+ assert config.enable_managed_keys is False
31
+
32
+ def test_can_enable_keys(self):
33
+ """Integrated keys can be explicitly enabled."""
34
+ # Arrange & Act
35
+ config = DeployConfigParams(enable_managed_keys=True)
36
+
37
+ # Assert
38
+ assert config.enable_managed_keys is True
39
+
40
+ def test_keys_included_in_dict_representation(self):
41
+ """Dictionary representation should include managed keys setting."""
42
+ # Arrange
43
+ config = DeployConfigParams(
44
+ agent_name="test-agent",
45
+ enable_managed_keys=True
46
+ )
47
+
48
+ # Act
49
+ result = config.to_dict()
50
+
51
+ # Assert
52
+ assert "enable_managed_keys" in result
53
+ assert result["enable_managed_keys"] is True
54
+
55
+ def test_keys_preserves_other_settings(self):
56
+ """Adding managed keys should not affect other configuration settings."""
57
+ # Arrange
58
+ config = DeployConfigParams(
59
+ agent_name="test-agent",
60
+ image="test:latest",
61
+ enable_krisp=True,
62
+ enable_managed_keys=True
63
+ )
64
+
65
+ # Act
66
+ result = config.to_dict()
67
+
68
+ # Assert
69
+ assert result["agent_name"] == "test-agent"
70
+ assert result["image"] == "test:latest"
71
+ assert result["enable_krisp"] is True
72
+ assert result["enable_managed_keys"] is True
73
+
74
+
75
+ class TestTOMLConfiguration:
76
+ """Test TOML configuration file parsing with managed keys."""
77
+
78
+ @pytest.fixture
79
+ def temp_config_file(self, tmp_path):
80
+ """Create a temporary TOML config file."""
81
+ config_path = tmp_path / "pcc-deploy.toml"
82
+ return config_path
83
+
84
+ def test_loads_keys_proxy_from_toml_when_enabled(self, temp_config_file):
85
+ """TOML file with managed keys enabled should be parsed correctly."""
86
+ # Arrange
87
+ config_content = """
88
+ agent_name = "test-agent"
89
+ image = "test:latest"
90
+ enable_managed_keys = true
91
+ """
92
+ temp_config_file.write_text(config_content)
93
+
94
+ # Act
95
+ with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
96
+ config = load_deploy_config_file()
97
+
98
+ # Assert
99
+ assert config is not None
100
+ assert config.enable_managed_keys is True
101
+
102
+ def test_loads_keys_proxy_from_toml_when_disabled(self, temp_config_file):
103
+ """TOML file with managed keys explicitly disabled should be parsed correctly."""
104
+ # Arrange
105
+ config_content = """
106
+ agent_name = "test-agent"
107
+ image = "test:latest"
108
+ enable_managed_keys = false
109
+ """
110
+ temp_config_file.write_text(config_content)
111
+
112
+ # Act
113
+ with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
114
+ config = load_deploy_config_file()
115
+
116
+ # Assert
117
+ assert config is not None
118
+ assert config.enable_managed_keys is False
119
+
120
+ def test_defaults_keys_proxy_when_not_in_toml(self, temp_config_file):
121
+ """TOML file without managed keys setting should default to disabled."""
122
+ # Arrange
123
+ config_content = """
124
+ agent_name = "test-agent"
125
+ image = "test:latest"
126
+ """
127
+ temp_config_file.write_text(config_content)
128
+
129
+ # Act
130
+ with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
131
+ config = load_deploy_config_file()
132
+
133
+ # Assert
134
+ assert config is not None
135
+ assert config.enable_managed_keys is False
136
+
137
+ def test_preserves_other_settings_with_keys_proxy(self, temp_config_file):
138
+ """Keys proxy setting should not interfere with other TOML settings."""
139
+ # Arrange
140
+ config_content = """
141
+ agent_name = "test-agent"
142
+ image = "test:latest"
143
+ enable_krisp = true
144
+ enable_managed_keys = true
145
+ secret_set = "my-secrets"
146
+
147
+ [scaling]
148
+ min_agents = 2
149
+ max_agents = 10
150
+ """
151
+ temp_config_file.write_text(config_content)
152
+
153
+ # Act
154
+ with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
155
+ config = load_deploy_config_file()
156
+
157
+ # Assert
158
+ assert config.agent_name == "test-agent"
159
+ assert config.image == "test:latest"
160
+ assert config.enable_krisp is True
161
+ assert config.enable_managed_keys is True
162
+ assert config.secret_set == "my-secrets"
163
+ assert config.scaling.min_agents == 2
164
+ assert config.scaling.max_agents == 10
165
+
166
+
167
+
168
+ class TestAPIIntegration:
169
+ """Test API client integration with managed keys."""
170
+
171
+ @pytest.fixture
172
+ def api_client(self):
173
+ """Create an API client instance."""
174
+ return _API(token="test-token", is_cli=True)
175
+
176
+ @pytest.mark.asyncio
177
+ async def test_deploy_payload_includes_keys_proxy(self, api_client):
178
+ """Deploy payload should include enableIntegratedKeys field."""
179
+ # Arrange
180
+ config = DeployConfigParams(
181
+ agent_name="test-agent",
182
+ image="test:latest",
183
+ enable_managed_keys=True
184
+ )
185
+
186
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
187
+ mock_request.return_value = {"success": True}
188
+
189
+ # Act
190
+ await api_client._deploy(config, "test-org", update=False)
191
+
192
+ # Assert
193
+ mock_request.assert_called_once()
194
+ payload = mock_request.call_args[1]["json"]
195
+ assert "enableIntegratedKeysProxy" in payload
196
+ assert payload["enableIntegratedKeysProxy"] is True
197
+
198
+ @pytest.mark.asyncio
199
+ async def test_update_payload_includes_keys_proxy(self, api_client):
200
+ """Update payload should include enableIntegratedKeys field."""
201
+ # Arrange
202
+ config = DeployConfigParams(
203
+ agent_name="test-agent",
204
+ image="test:latest",
205
+ enable_managed_keys=True
206
+ )
207
+
208
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
209
+ mock_request.return_value = {"success": True}
210
+
211
+ # Act
212
+ await api_client._deploy(config, "test-org", update=True)
213
+
214
+ # Assert
215
+ mock_request.assert_called_once()
216
+ payload = mock_request.call_args[1]["json"]
217
+ assert "enableIntegratedKeysProxy" in payload
218
+ assert payload["enableIntegratedKeysProxy"] is True
219
+
220
+ @pytest.mark.asyncio
221
+ async def test_disabled_keys_proxy_sends_false(self, api_client):
222
+ """When managed keys is disabled, API should send false value."""
223
+ # Arrange
224
+ config = DeployConfigParams(
225
+ agent_name="test-agent",
226
+ image="test:latest",
227
+ enable_managed_keys=False
228
+ )
229
+
230
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
231
+ mock_request.return_value = {"success": True}
232
+
233
+ # Act
234
+ await api_client._deploy(config, "test-org", update=False)
235
+
236
+ # Assert
237
+ mock_request.assert_called_once()
238
+ payload = mock_request.call_args[1]["json"]
239
+ assert "enableIntegratedKeysProxy" in payload
240
+ assert payload["enableIntegratedKeysProxy"] is False
241
+
242
+
243
+ class TestAgentStatusDisplay:
244
+ """Test agent status command displays managed keys status."""
245
+
246
+ @pytest.fixture
247
+ def mock_api(self):
248
+ """Mock the API agent method."""
249
+ with patch("src.pipecatcloud.cli.commands.agent.API") as mock_api:
250
+ yield mock_api
251
+
252
+ def test_displays_enabled_keys_proxy(self, mock_api, capsys):
253
+ """Agent status should display when managed keys is enabled."""
254
+ # Arrange
255
+ from src.pipecatcloud.cli.commands.agent import status
256
+
257
+ mock_api.agent = AsyncMock(return_value=({
258
+ "ready": True,
259
+ "deployment": {
260
+ "manifest": {
261
+ "spec": {
262
+ "integratedKeysProxy": {
263
+ "enabled": True
264
+ }
265
+ }
266
+ }
267
+ },
268
+ "autoScaling": {"minAgents": 0, "maxAgents": 1} # Avoid undefined variable bug
269
+ }, None))
270
+
271
+ # Act
272
+ status(agent_name="test-agent", organization="test-org")
273
+
274
+ # Assert
275
+ captured = capsys.readouterr()
276
+ assert "Managed Keys" in captured.out
277
+ assert "Enabled" in captured.out
278
+
279
+ def test_displays_disabled_keys_proxy(self, mock_api, capsys):
280
+ """Agent status should display when managed keys is disabled."""
281
+ # Arrange
282
+ from src.pipecatcloud.cli.commands.agent import status
283
+
284
+ mock_api.agent = AsyncMock(return_value=({
285
+ "ready": True,
286
+ "deployment": {
287
+ "manifest": {
288
+ "spec": {}
289
+ }
290
+ },
291
+ "autoScaling": {"minAgents": 0, "maxAgents": 1} # Avoid undefined variable bug
292
+ }, None))
293
+
294
+ # Act
295
+ status(agent_name="test-agent", organization="test-org")
296
+
297
+ # Assert
298
+ captured = capsys.readouterr()
299
+ assert "Managed Keys" in captured.out
300
+ assert "Disabled" in captured.out
301
+
302
+ def test_handles_boolean_keys_proxy_value(self, mock_api, capsys):
303
+ """Agent status should handle boolean value for backward compatibility."""
304
+ # Arrange
305
+ from src.pipecatcloud.cli.commands.agent import status
306
+
307
+ mock_api.agent = AsyncMock(return_value=({
308
+ "ready": True,
309
+ "deployment": {
310
+ "manifest": {
311
+ "spec": {
312
+ "integratedKeysProxy": True
313
+ }
314
+ }
315
+ },
316
+ "autoScaling": {"minAgents": 0, "maxAgents": 1} # Avoid undefined variable bug
317
+ }, None))
318
+
319
+ # Act
320
+ status(agent_name="test-agent", organization="test-org")
321
+
322
+ # Assert
323
+ captured = capsys.readouterr()
324
+ assert "Managed Keys" in captured.out
325
+ assert "Enabled" in captured.out
326
+
327
+
328
+ class TestBackwardCompatibility:
329
+ """Test backward compatibility with existing deployments."""
330
+
331
+ def test_existing_config_without_keys_proxy_works(self):
332
+ """Existing configs without managed keys field should work unchanged."""
333
+ # Arrange
334
+ config = DeployConfigParams(
335
+ agent_name="test-agent",
336
+ image="test:latest",
337
+ enable_krisp=True
338
+ )
339
+
340
+ # Act
341
+ result = config.to_dict()
342
+
343
+ # Assert
344
+ assert result["enable_krisp"] is True
345
+ assert result["enable_managed_keys"] is False
346
+
347
+ def test_api_response_without_keys_proxy_doesnt_crash(self):
348
+ """Agent status should handle API responses without managed keys field."""
349
+ # Arrange
350
+ data = {
351
+ "ready": True,
352
+ "deployment": {
353
+ "manifest": {
354
+ "spec": {
355
+ "image": "test:latest"
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ # Act - simulate the logic from agent.py
362
+ integrated_keys = data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("integratedKeysProxy", {})
363
+ if isinstance(integrated_keys, dict):
364
+ keys_proxy_enabled = integrated_keys.get("enabled", False)
365
+ else:
366
+ keys_proxy_enabled = bool(integrated_keys)
367
+
368
+ # Assert
369
+ assert keys_proxy_enabled is False
370
+
371
+ @pytest.fixture
372
+ def temp_legacy_config(self, tmp_path):
373
+ """Create a legacy TOML config without managed keys."""
374
+ config_path = tmp_path / "pcc-deploy.toml"
375
+ config_content = """
376
+ agent_name = "legacy-agent"
377
+ image = "legacy:latest"
378
+ enable_krisp = true
379
+
380
+ [scaling]
381
+ min_instances = 1
382
+ max_instances = 5
383
+ """
384
+ config_path.write_text(config_content)
385
+ return config_path
386
+
387
+ def test_legacy_toml_loads_with_defaults(self, temp_legacy_config):
388
+ """Legacy TOML files should load with managed keys defaulting to disabled."""
389
+ # Arrange & Act
390
+ with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_legacy_config)):
391
+ config = load_deploy_config_file()
392
+
393
+ # Assert
394
+ assert config is not None
395
+ assert config.agent_name == "legacy-agent"
396
+ assert config.enable_krisp is True
397
+ assert config.enable_managed_keys is False
398
+ assert config.scaling.min_agents == 1 # Converted from min_instances
399
+
400
+
401
+ class TestErrorHandling:
402
+ """Test error handling and edge cases."""
403
+
404
+ @pytest.mark.asyncio
405
+ async def test_api_error_during_deploy_with_keys_proxy(self):
406
+ """API errors during deployment should be handled properly."""
407
+ # Arrange
408
+ api = _API(token="test-token", is_cli=True)
409
+ config = DeployConfigParams(
410
+ agent_name="test-agent",
411
+ image="test:latest",
412
+ enable_managed_keys=True
413
+ )
414
+
415
+ with patch.object(api, "_base_request", new_callable=AsyncMock) as mock_request:
416
+ mock_request.side_effect = Exception("Network error")
417
+
418
+ # Act & Assert
419
+ with pytest.raises(Exception, match="Network error"):
420
+ await api._deploy(config, "test-org", update=False)
421
+
422
+ def test_simultaneous_krisp_and_keys_proxy(self):
423
+ """Both Krisp and Keys Proxy can be enabled simultaneously."""
424
+ # Arrange
425
+ config = DeployConfigParams(
426
+ agent_name="test-agent",
427
+ image="test:latest",
428
+ enable_krisp=True,
429
+ enable_managed_keys=True
430
+ )
431
+
432
+ # Act
433
+ result = config.to_dict()
434
+
435
+ # Assert
436
+ assert result["enable_krisp"] is True
437
+ assert result["enable_managed_keys"] is True
@@ -1,84 +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:
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
@@ -1,40 +0,0 @@
1
- name: Build and publish Python package
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- permissions:
8
- contents: read
9
-
10
- jobs:
11
- release-build:
12
- runs-on: ubuntu-latest
13
-
14
- steps:
15
- - uses: actions/checkout@v4
16
-
17
- - uses: actions/setup-python@v5
18
- with:
19
- python-version: '3.10'
20
- - name: Setup virtual environment
21
- run: |
22
- python -m venv .venv
23
- - name: Install basic Python dependencies
24
- run: |
25
- source .venv/bin/activate
26
- python -m pip install --upgrade pip
27
- pip install -r dev-requirements.txt
28
- - name: Build project
29
- run: |
30
- source .venv/bin/activate
31
- python -m build
32
- - name: Install project and other Python dependencies
33
- run: |
34
- source .venv/bin/activate
35
- pip install --editable .
36
- - name: Upload distributions
37
- uses: actions/upload-artifact@v4
38
- with:
39
- name: release-dists
40
- path: dist/
File without changes
File without changes
File without changes
File without changes
File without changes