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.
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/CHANGELOG.md +26 -0
- {pipecatcloud-0.2.12/src/pipecatcloud.egg-info → pipecatcloud-0.2.13}/PKG-INFO +1 -1
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/api.py +59 -3
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/deploy.py +16 -11
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/organizations.py +195 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/secrets.py +32 -31
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/config.py +1 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13/src/pipecatcloud.egg-info}/PKG-INFO +1 -1
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_api_client.py +92 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/.gitignore +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/LICENSE +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/README.md +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/pyproject.toml +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/pytest.ini +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/setup.cfg +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/_utils/regions.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/agent.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/docker.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/SOURCES.txt +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/__init__.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_managed_keys.py +0 -0
- {pipecatcloud-0.2.12 → pipecatcloud-0.2.13}/tests/test_regions.py +0 -0
- {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
|
|
@@ -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
|
-
|
|
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
|
|
385
|
+
# Handle region - if not specified, API will use org's default region
|
|
385
386
|
deploy_region = region or partial_config.region
|
|
386
|
-
|
|
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
|
|
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]
|
|
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
|
-
#
|
|
188
|
-
#
|
|
189
|
-
|
|
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 '{
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
#
|
|
543
|
-
#
|
|
544
|
-
|
|
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 '{
|
|
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=
|
|
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
|
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|