pipecatcloud 0.2.12__tar.gz → 0.2.13__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 (56) hide show
  1. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/CHANGELOG.md +26 -0
  2. {pipecatcloud-0.2.12/src/pipecatcloud.egg-info → pipecatcloud-0.2.13}/PKG-INFO +1 -1
  3. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/api.py +59 -3
  4. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/deploy.py +16 -11
  5. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/organizations.py +195 -0
  6. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/secrets.py +32 -31
  7. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/config.py +1 -0
  8. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13/src/pipecatcloud.egg-info}/PKG-INFO +1 -1
  9. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_api_client.py +92 -0
  10. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.github/workflows/publish-pypi.yml +0 -0
  11. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.github/workflows/publish-test.yml +0 -0
  12. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.github/workflows/tests.yml +0 -0
  13. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.gitignore +0 -0
  14. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/LICENSE +0 -0
  15. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/README.md +0 -0
  16. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/pipecat-cloud.png +0 -0
  17. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/pyproject.toml +0 -0
  18. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/pytest.ini +0 -0
  19. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/setup.cfg +0 -0
  20. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/__init__.py +0 -0
  21. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/__main__.py +0 -0
  22. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/__version__.py +0 -0
  23. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/__init__.py +0 -0
  24. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/async_utils.py +0 -0
  25. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  26. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/console_utils.py +0 -0
  27. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
  28. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/regions.py +0 -0
  29. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/agent.py +0 -0
  30. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/__init__.py +0 -0
  31. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/api.py +0 -0
  32. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  33. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/agent.py +0 -0
  34. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/auth.py +0 -0
  35. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/docker.py +0 -0
  36. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/regions.py +0 -0
  37. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/config.py +0 -0
  38. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/entry_point.py +0 -0
  39. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/constants.py +0 -0
  40. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/exception.py +0 -0
  41. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/py.typed +0 -0
  42. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/session.py +0 -0
  43. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  44. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/SOURCES.txt +0 -0
  45. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  46. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  47. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/requires.txt +0 -0
  48. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  49. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/__init__.py +0 -0
  50. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_agent_sessions.py +0 -0
  51. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_agent_stop.py +0 -0
  52. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_docker_commands.py +0 -0
  53. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_krisp_viva.py +0 -0
  54. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_managed_keys.py +0 -0
  55. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_regions.py +0 -0
  56. {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/uv.lock +0 -0
@@ -5,6 +5,32 @@ 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.13] - 2025-12-03
9
+
10
+ ### Added
11
+
12
+ - Added `pcc organizations properties` commands for managing organization-level
13
+ configuration:
14
+
15
+ - `properties list` - Display current property values
16
+ - `properties set <name> <value>` - Update a property value
17
+ - `properties schema` - Show available properties with metadata and allowed
18
+ values
19
+
20
+ - Added `pcc organizations default-region [region]` convenience command to get or
21
+ set the organization's default region.
22
+
23
+ ### Changed
24
+
25
+ - Region is now optional for `secrets set`, `secrets image-pull-secret`, and
26
+ `deploy` commands. When `--region` is not specified, the API now uses the
27
+ organization's configured default region instead of the CLI defaulting to
28
+ `us-west`. The confirmation display shows the actual region that will be used
29
+ (e.g., "us-west (organization default)").
30
+
31
+ - Removed deprecation warnings about region defaulting to `us-west`. The
32
+ organization's default region is now the source of truth.
33
+
8
34
  ## [0.2.12] - 2025-11-26
9
35
 
10
36
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.12
3
+ Version: 0.2.13
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/
@@ -266,11 +266,13 @@ class _API:
266
266
  """
267
267
  return self.create_api_method(self._secrets_list)
268
268
 
269
- async def _secrets_upsert(self, data: dict, set_name: str, org: str, region: str) -> dict:
269
+ async def _secrets_upsert(self, data: dict, set_name: str, org: str, region: Optional[str] = None) -> dict:
270
270
  url = f"{self.construct_api_url('secrets_path').format(org=org)}/{set_name}"
271
271
 
272
- # Add region to data payload
273
- data = {**data, "region": region}
272
+ # Add region to data payload only if explicitly provided
273
+ # If not provided, API will use org's default region
274
+ if region:
275
+ data = {**data, "region": region}
274
276
 
275
277
  return await self._base_request("PUT", url, json=data) or {}
276
278
 
@@ -506,3 +508,57 @@ class _API:
506
508
  List of region objects with 'code' and 'display_name' fields
507
509
  """
508
510
  return self.create_api_method(self._regions)
511
+
512
+ # Organization Properties
513
+
514
+ async def _properties(self, org: str) -> dict | None:
515
+ url = self.construct_api_url("properties_path").format(org=org)
516
+ result = await self._base_request("GET", url, not_found_is_empty=True)
517
+ if result and "properties" in result:
518
+ return result["properties"]
519
+ return None
520
+
521
+ @property
522
+ def properties(self):
523
+ """Get current organization properties
524
+ Args:
525
+ org: Organization ID
526
+ Returns:
527
+ Dict of property names to current values
528
+ """
529
+ return self.create_api_method(self._properties)
530
+
531
+ async def _properties_schema(self, org: str) -> dict | None:
532
+ url = f"{self.construct_api_url('properties_path').format(org=org)}/schema"
533
+ result = await self._base_request("GET", url, not_found_is_empty=True)
534
+ if result and "properties" in result:
535
+ return result["properties"]
536
+ return None
537
+
538
+ @property
539
+ def properties_schema(self):
540
+ """Get organization properties schema with metadata
541
+ Args:
542
+ org: Organization ID
543
+ Returns:
544
+ Dict of property names to schema info (type, description, currentValue, default, availableValues)
545
+ """
546
+ return self.create_api_method(self._properties_schema)
547
+
548
+ async def _properties_update(self, org: str, properties: dict) -> dict | None:
549
+ url = self.construct_api_url("properties_path").format(org=org)
550
+ result = await self._base_request("PATCH", url, json=properties)
551
+ if result and "properties" in result:
552
+ return result["properties"]
553
+ return None
554
+
555
+ @property
556
+ def properties_update(self):
557
+ """Update organization properties
558
+ Args:
559
+ org: Organization ID
560
+ properties: Dict of property names to new values
561
+ Returns:
562
+ Updated properties dict
563
+ """
564
+ return self.create_api_method(self._properties_update)
@@ -38,6 +38,7 @@ ALIVE_CHECK_SLEEP = 5
38
38
 
39
39
  async def _deploy(params: DeployConfigParams, org, force: bool = False):
40
40
  existing_agent = False
41
+ deployed_region = None # Will be captured from API response
41
42
 
42
43
  # Check for an existing deployment with this agent name
43
44
  with Live(
@@ -381,18 +382,12 @@ def create_deploy_command(app: typer.Typer):
381
382
  partial_config.enable_managed_keys = managed_keys or partial_config.enable_managed_keys
382
383
  partial_config.agent_profile = profile or partial_config.agent_profile
383
384
 
384
- # Handle region with warning if not specified
385
+ # Handle region - if not specified, API will use org's default region
385
386
  deploy_region = region or partial_config.region
386
- if not deploy_region:
387
- logger.warning(
388
- "Region not specified, defaulting to 'us-west'. Please use --region to specify explicitly. "
389
- "This default will be required in a future version."
390
- )
391
- deploy_region = "us-west"
392
- partial_config.region = deploy_region
387
+ partial_config.region = deploy_region # Can be None, API will use org default
393
388
 
394
- # Validate region against API
395
- if not await validate_region(deploy_region):
389
+ # Validate region if explicitly provided
390
+ if deploy_region and not await validate_region(deploy_region):
396
391
  valid_regions = await get_region_codes()
397
392
  console.error(
398
393
  f"Invalid region '{deploy_region}'. Valid regions are: {', '.join(valid_regions)}"
@@ -432,11 +427,21 @@ def create_deploy_command(app: typer.Typer):
432
427
  else:
433
428
  table.add_row("Max agents", "[dim]Use existing or default[/dim]")
434
429
 
430
+ # Resolve region display - fetch org default if not explicitly specified
431
+ if partial_config.region:
432
+ region_display = f"[green]{partial_config.region}[/green]"
433
+ else:
434
+ # Fetch org's default region to show user what will be used
435
+ props, error = await API.properties(org)
436
+ if error:
437
+ return typer.Exit()
438
+ region_display = f"[green]{props['defaultRegion']}[/green] [dim](organization default)[/dim]"
439
+
435
440
  content = Group(
436
441
  (f"[bold white]Agent name:[/bold white] [green]{partial_config.agent_name}[/green]"),
437
442
  (f"[bold white]Image:[/bold white] [green]{partial_config.image}[/green]"),
438
443
  (f"[bold white]Organization:[/bold white] [green]{org}[/green]"),
439
- (f"[bold white]Region:[/bold white] [green]{partial_config.region}[/green]"),
444
+ (f"[bold white]Region:[/bold white] {region_display}"),
440
445
  (f"[bold white]Secret set:[/bold white] {'[dim]None[/dim]' if not partial_config.secret_set else '[green] '+ partial_config.secret_set + '[/green]'}"),
441
446
  (f"[bold white]Image pull secret:[/bold white] {'[dim]None[/dim]' if not partial_config.image_credentials else '[green]' + partial_config.image_credentials + '[/green]'}"),
442
447
  (f"[bold white]Agent profile:[/bold white] {'[dim]None[/dim]' if not partial_config.agent_profile else '[green]' + partial_config.agent_profile + '[/green]'}"),
@@ -25,7 +25,9 @@ organization_cli = typer.Typer(
25
25
  name="organizations", help="User organizations", no_args_is_help=True
26
26
  )
27
27
  keys_cli = typer.Typer(name="keys", help="API key management commands", no_args_is_help=True)
28
+ properties_cli = typer.Typer(name="properties", help="Organization property management", no_args_is_help=True)
28
29
  organization_cli.add_typer(keys_cli)
30
+ organization_cli.add_typer(properties_cli)
29
31
 
30
32
 
31
33
  # ---- Commands
@@ -385,3 +387,196 @@ async def use_key(
385
387
  except Exception as e:
386
388
  logger.debug(e)
387
389
  console.error("Unable to set default key in local config. Please contact support.")
390
+
391
+
392
+ # ---- Properties Commands ----
393
+
394
+
395
+ @properties_cli.command(name="list", help="List current organization property values.")
396
+ @synchronizer.create_blocking
397
+ @requires_login
398
+ async def properties_list(
399
+ organization: str = typer.Option(
400
+ None,
401
+ "--organization",
402
+ "-o",
403
+ help="Organization to list properties for",
404
+ ),
405
+ ):
406
+ org = organization or config.get("org")
407
+
408
+ with console.status(
409
+ f"[dim]Fetching properties for organization: [bold]'{org}'[/bold][/dim]", spinner="dots"
410
+ ):
411
+ data, error = await API.properties(org)
412
+
413
+ if error:
414
+ return typer.Exit()
415
+
416
+ if not data:
417
+ console.print("[dim]No properties configured.[/dim]")
418
+ return
419
+
420
+ table = Table(
421
+ show_header=True,
422
+ show_lines=False,
423
+ border_style="dim",
424
+ box=box.SIMPLE,
425
+ )
426
+ table.add_column("Property", style="cyan")
427
+ table.add_column("Value", style="white")
428
+
429
+ for prop_name, prop_value in data.items():
430
+ table.add_row(prop_name, str(prop_value))
431
+
432
+ console.success(table, title_extra=f"Properties for organization: {org}")
433
+
434
+
435
+ @properties_cli.command(name="schema", help="Show available properties with metadata.")
436
+ @synchronizer.create_blocking
437
+ @requires_login
438
+ async def properties_schema(
439
+ organization: str = typer.Option(
440
+ None,
441
+ "--organization",
442
+ "-o",
443
+ help="Organization to show properties schema for",
444
+ ),
445
+ ):
446
+ org = organization or config.get("org")
447
+
448
+ with console.status(
449
+ f"[dim]Fetching properties schema for organization: [bold]'{org}'[/bold][/dim]",
450
+ spinner="dots",
451
+ ):
452
+ data, error = await API.properties_schema(org)
453
+
454
+ if error:
455
+ return typer.Exit()
456
+
457
+ if not data:
458
+ console.print("[dim]No properties available.[/dim]")
459
+ return
460
+
461
+ table = Table(
462
+ show_header=True,
463
+ show_lines=True,
464
+ border_style="dim",
465
+ box=box.SIMPLE,
466
+ )
467
+ table.add_column("Property", style="cyan")
468
+ table.add_column("Type")
469
+ table.add_column("Current Value", style="green")
470
+ table.add_column("Default")
471
+ table.add_column("Description")
472
+
473
+ for prop_name, prop_info in data.items():
474
+ current = prop_info.get("currentValue", "")
475
+ default = prop_info.get("default", "")
476
+ available = prop_info.get("availableValues")
477
+
478
+ # Show available values in description if present
479
+ description = prop_info.get("description", "")
480
+ if available:
481
+ description += f"\n[dim]Available: {', '.join(str(v) for v in available)}[/dim]"
482
+
483
+ table.add_row(
484
+ prop_name,
485
+ prop_info.get("type", ""),
486
+ str(current) if current is not None else "[dim]not set[/dim]",
487
+ str(default) if default is not None else "",
488
+ description,
489
+ )
490
+
491
+ console.success(table, title_extra=f"Properties schema for organization: {org}")
492
+
493
+
494
+ @properties_cli.command(name="set", help="Update an organization property.")
495
+ @synchronizer.create_blocking
496
+ @requires_login
497
+ async def properties_set(
498
+ property_name: str = typer.Argument(..., help="Name of the property to set"),
499
+ value: str = typer.Argument(..., help="Value to set"),
500
+ organization: str = typer.Option(
501
+ None,
502
+ "--organization",
503
+ "-o",
504
+ help="Organization to update property for",
505
+ ),
506
+ ):
507
+ org = organization or config.get("org")
508
+
509
+ with console.status(
510
+ f"[dim]Updating property [bold]'{property_name}'[/bold] for organization: [bold]'{org}'[/bold][/dim]",
511
+ spinner="dots",
512
+ ):
513
+ data, error = await API.properties_update(org, {property_name: value})
514
+
515
+ if error:
516
+ return typer.Exit()
517
+
518
+ if not data:
519
+ console.error("Failed to update property.")
520
+ return typer.Exit(1)
521
+
522
+ new_value = data.get(property_name, value)
523
+ console.success(
524
+ f"Property [bold cyan]{property_name}[/bold cyan] set to [bold green]{new_value}[/bold green]"
525
+ )
526
+
527
+
528
+ # ---- Convenience Commands ----
529
+
530
+
531
+ @organization_cli.command(name="default-region", help="Get or set the default region for an organization.")
532
+ @synchronizer.create_blocking
533
+ @requires_login
534
+ async def default_region(
535
+ region: str = typer.Argument(None, help="Region to set as default (omit to show current)"),
536
+ organization: str = typer.Option(
537
+ None,
538
+ "--organization",
539
+ "-o",
540
+ help="Organization to configure",
541
+ ),
542
+ ):
543
+ org = organization or config.get("org")
544
+
545
+ if region:
546
+ # Set the default region
547
+ with console.status(
548
+ f"[dim]Setting default region to [bold]'{region}'[/bold] for organization: [bold]'{org}'[/bold][/dim]",
549
+ spinner="dots",
550
+ ):
551
+ data, error = await API.properties_update(org, {"defaultRegion": region})
552
+
553
+ if error:
554
+ return typer.Exit()
555
+
556
+ if not data:
557
+ console.error("Failed to update default region.")
558
+ return typer.Exit(1)
559
+
560
+ console.success(f"Default region set to [bold green]{data.get('defaultRegion', region)}[/bold green]")
561
+ else:
562
+ # Show the current default region
563
+ with console.status(
564
+ f"[dim]Fetching default region for organization: [bold]'{org}'[/bold][/dim]",
565
+ spinner="dots",
566
+ ):
567
+ data, error = await API.properties_schema(org)
568
+
569
+ if error:
570
+ return typer.Exit()
571
+
572
+ if not data or "defaultRegion" not in data:
573
+ console.print("[dim]No default region configured.[/dim]")
574
+ return
575
+
576
+ prop = data["defaultRegion"]
577
+ current = prop.get("currentValue", prop.get("default", "not set"))
578
+ available = prop.get("availableValues", [])
579
+
580
+ console.print(f"Default region: [bold green]{current}[/bold green]")
581
+ if available:
582
+ console.print(f"[dim]Available regions: {', '.join(available)}[/dim]")
@@ -184,21 +184,12 @@ async def set(
184
184
 
185
185
  validate_secrets(secrets_dict)
186
186
 
187
- # Handle region with warning if not specified
188
- # CLI always sends region explicitly (PUT semantics), API validates it matches for existing sets
189
- secret_region = region
190
- if not secret_region:
191
- logger.warning(
192
- "Region not specified, defaulting to 'us-west'. Please use --region to specify explicitly. "
193
- "This default will be required in a future version."
194
- )
195
- secret_region = "us-west"
196
-
197
- # Validate region against API
198
- if not await validate_region(secret_region):
187
+ # Validate region if explicitly provided
188
+ # If not provided, API will use org's default region
189
+ if region and not await validate_region(region):
199
190
  valid_regions = await get_region_codes()
200
191
  console.print(
201
- f"[red]Invalid region '{secret_region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
192
+ f"[red]Invalid region '{region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
202
193
  )
203
194
  return typer.Exit(1)
204
195
 
@@ -213,7 +204,14 @@ async def set(
213
204
  table.add_row(key, preview)
214
205
 
215
206
  console.print(f"\n[bold white]Secret Set:[/bold white] {name}")
216
- console.print(f"[bold white]Region:[/bold white] {secret_region}\n")
207
+ if region:
208
+ console.print(f"[bold white]Region:[/bold white] {region}\n")
209
+ else:
210
+ # Fetch org's default region to show user what will be used
211
+ props, error = await API.properties(org)
212
+ if error:
213
+ return typer.Exit()
214
+ console.print(f"[bold white]Region:[/bold white] {props['defaultRegion']} [dim](organization default)[/dim]\n")
217
215
  console.print(
218
216
  Panel(
219
217
  table,
@@ -255,6 +253,7 @@ async def set(
255
253
  console.print("[bold red]Secret set creation cancelled[/bold red]")
256
254
  return typer.Exit(1)
257
255
 
256
+ used_region = None
258
257
  with console.status(
259
258
  f"[dim]{'Modifying' if existing_set else 'Creating'} secret set [bold]'{name}'[/bold][/dim]",
260
259
  spinner="dots",
@@ -269,14 +268,19 @@ async def set(
269
268
  },
270
269
  set_name=name,
271
270
  org=org,
272
- region=secret_region,
271
+ region=region, # Pass region if provided, otherwise API uses org default
273
272
  )
274
273
 
275
274
  if error:
276
275
  return typer.Exit()
277
276
 
277
+ # Capture the region that was used (from API response)
278
+ if data and "region" in data:
279
+ used_region = data["region"]
280
+
278
281
  action = "created" if not existing_set else "modified"
279
- message = f"Secret set [bold green]'{name}'[/bold green] {action} successfully"
282
+ region_info = f" in [bold cyan]{used_region}[/bold cyan]" if used_region else ""
283
+ message = f"Secret set [bold green]'{name}'[/bold green] {action} successfully{region_info}"
280
284
  if action == "modified":
281
285
  message += "\n[bold white]You must re-deploy any agents using this secret set for changes to take effect[/bold white]"
282
286
  else:
@@ -539,25 +543,17 @@ async def image_pull_secret(
539
543
  if base64encode:
540
544
  credentials = base64.b64encode(credentials.encode()).decode()
541
545
 
542
- # Handle region with warning if not specified
543
- # CLI always sends region explicitly (PUT semantics)
544
- secret_region = region
545
- if not secret_region:
546
- logger.warning(
547
- "Region not specified, defaulting to 'us-west'. Please use --region to specify explicitly. "
548
- "This default will be required in a future version."
549
- )
550
- secret_region = "us-west"
551
-
552
- # Validate region against API
553
- if not await validate_region(secret_region):
546
+ # Validate region if explicitly provided
547
+ # If not provided, API will use org's default region
548
+ if region and not await validate_region(region):
554
549
  valid_regions = await get_region_codes()
555
550
  console.print(
556
- f"[red]Invalid region '{secret_region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
551
+ f"[red]Invalid region '{region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
557
552
  )
558
553
  return typer.Exit(1)
559
554
 
560
555
  # Check if secret already exists
556
+ used_region = None
561
557
  with Live(
562
558
  console.status(
563
559
  f"[dim]Checking if image pull secret '{name}' already exists[/dim]", spinner="dots"
@@ -590,12 +586,17 @@ async def image_pull_secret(
590
586
  data={"isImagePullSecret": True, "secretValue": credentials, "host": host},
591
587
  set_name=name,
592
588
  org=org,
593
- region=secret_region,
589
+ region=region, # Pass region if provided, otherwise API uses org default
594
590
  )
595
591
 
596
592
  if error:
597
593
  return typer.Exit()
598
594
 
595
+ # Capture the region that was used (from API response)
596
+ if data and "region" in data:
597
+ used_region = data["region"]
598
+
599
+ region_info = f" in [bold cyan]{used_region}[/bold cyan]" if used_region else ""
599
600
  console.success(
600
- f"Image pull secret [bold green]'{name}'[/bold green] for [bold green]{host}[/bold green] created successfully.",
601
+ f"Image pull secret [bold green]'{name}'[/bold green] for [bold green]{host}[/bold green] created successfully{region_info}.",
601
602
  )
@@ -35,6 +35,7 @@ _SETTINGS = {
35
35
  "api_keys_path": _Setting("/v1/organizations/{org}/apiKeys"),
36
36
  "secrets_path": _Setting("/v1/organizations/{org}/secrets"),
37
37
  "regions_path": _Setting("/v1/organizations/{org}/regions"),
38
+ "properties_path": _Setting("/v1/organizations/{org}/properties"),
38
39
  }
39
40
 
40
41
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.12
3
+ Version: 0.2.13
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/
@@ -111,6 +111,25 @@ class TestAPISecretsRegions:
111
111
  assert payload["secretValue"] == "value"
112
112
  assert payload["isImagePullSecret"] is False
113
113
 
114
+ @pytest.mark.asyncio
115
+ async def test_secrets_upsert_without_region(self, api_client):
116
+ """Secrets upsert without region should not include region in payload (API uses org default)."""
117
+ # Arrange
118
+ data = {"secretKey": "KEY", "secretValue": "value"}
119
+
120
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
121
+ mock_request.return_value = {"status": "OK", "region": "us-west"}
122
+
123
+ # Act
124
+ await api_client._secrets_upsert(
125
+ data=data, set_name="my-secrets", org="test-org", region=None
126
+ )
127
+
128
+ # Assert
129
+ mock_request.assert_called_once()
130
+ payload = mock_request.call_args[1]["json"]
131
+ assert "region" not in payload # Region omitted, API uses org default
132
+
114
133
 
115
134
  class TestAPIServicesRegions:
116
135
  """Test API client services/agents methods with region support."""
@@ -270,3 +289,76 @@ class TestAPIRegionWithOtherParameters:
270
289
  assert payload["secretSet"] == "my-secrets"
271
290
  assert payload["imagePullSecretSet"] == "my-creds"
272
291
  assert payload["enableIntegratedKeysProxy"] is True
292
+
293
+
294
+ class TestAPIProperties:
295
+ """Test API client organization properties methods."""
296
+
297
+ @pytest.fixture
298
+ def api_client(self):
299
+ """Create an API client instance."""
300
+ return _API(token="test-token", is_cli=True)
301
+
302
+ @pytest.mark.asyncio
303
+ async def test_properties_returns_properties_dict(self, api_client):
304
+ """Properties endpoint should return the properties object."""
305
+ # Arrange
306
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
307
+ mock_request.return_value = {"properties": {"defaultRegion": "us-west"}}
308
+
309
+ # Act
310
+ result = await api_client._properties(org="test-org")
311
+
312
+ # Assert
313
+ assert result == {"defaultRegion": "us-west"}
314
+ mock_request.assert_called_once()
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_properties_schema_returns_schema_dict(self, api_client):
318
+ """Properties schema endpoint should return schema with metadata."""
319
+ # Arrange
320
+ mock_schema = {
321
+ "defaultRegion": {
322
+ "type": "string",
323
+ "description": "Default region for deployments",
324
+ "readOnly": False,
325
+ "currentValue": "us-west",
326
+ "default": "us-west",
327
+ "availableValues": ["us-west", "eu-central"]
328
+ }
329
+ }
330
+
331
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
332
+ mock_request.return_value = {"properties": mock_schema}
333
+
334
+ # Act
335
+ result = await api_client._properties_schema(org="test-org")
336
+
337
+ # Assert
338
+ assert result == mock_schema
339
+ mock_request.assert_called_once()
340
+ # Verify it called the /schema endpoint
341
+ call_url = mock_request.call_args[0][1]
342
+ assert "/schema" in call_url
343
+
344
+ @pytest.mark.asyncio
345
+ async def test_properties_update_sends_patch_request(self, api_client):
346
+ """Properties update should send PATCH request with properties payload."""
347
+ # Arrange
348
+ with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
349
+ mock_request.return_value = {"properties": {"defaultRegion": "eu-central"}}
350
+
351
+ # Act
352
+ result = await api_client._properties_update(
353
+ org="test-org",
354
+ properties={"defaultRegion": "eu-central"}
355
+ )
356
+
357
+ # Assert
358
+ assert result == {"defaultRegion": "eu-central"}
359
+ mock_request.assert_called_once()
360
+ # Verify PATCH method
361
+ assert mock_request.call_args[0][0] == "PATCH"
362
+ # Verify payload
363
+ payload = mock_request.call_args[1]["json"]
364
+ assert payload == {"defaultRegion": "eu-central"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes