Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ repos:
rev: v2.9.0
hooks:
- id: pip-audit
# Temporary workaround: ignoring pip vulnerability GHSA-4xh5-x5gv-qwph (pip 25.2).
# Remove this ignore once a patched version of pip is available.
# Ensure the hook environment uses a patched pip version.
additional_dependencies: ["pip>=26.1"]
args: [--format=json, --ignore-vuln=GHSA-4xh5-x5gv-qwph]

# Local hooks for project-specific tasks
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ clean:
rm -rf *.egg-info/
rm -rf .coverage
rm -rf htmlcov/
find . -type d -name __pycache__ -delete
find . -type f -name "*.pyc" -delete
find . -type d -name __pycache__ -delete

build: clean
python -m build
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ workato connections list

# Check project status
workato workspace

# Configure project export defaults
workato projects config --include-tags --no-include-test-cases

# Show current project export defaults
workato projects config show
```

## Next Steps
Expand Down
2 changes: 2 additions & 0 deletions docs/COMMAND_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ workato init # Initialize CLI configuration
workato workspace # Show current workspace info
workato pull # Pull latest from remote
workato push [--restart-recipes] # Push local changes (recipes won't restart by default)
workato projects config --include-tags|--no-include-tags [--include-test-cases|--no-include-test-cases]
workato projects config show [--output-mode table|json]
```

### Recipe Management
Expand Down
79 changes: 79 additions & 0 deletions src/workato_platform_cli/cli/commands/projects/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,85 @@ async def switch(
click.echo(f"❌ Failed to switch to project '{selected_project_name}': {e}")


@projects.group(name="config", invoke_without_command=True)
@click.option(
"--include-tags/--no-include-tags",
default=None,
help=(
"Set export_include_tags default in .workatoenv for project export manifests"
),
)
@click.option(
"--include-test-cases/--no-include-test-cases",
default=None,
help=(
"Set export_include_test_cases default in .workatoenv for project "
"export manifests"
),
)
@handle_cli_exceptions
@inject
async def set_project_config(
include_tags: bool | None = None,
include_test_cases: bool | None = None,
config_manager: ConfigManager = Provide[Container.config_manager],
) -> None:
"""Set project export defaults in local .workatoenv config."""

Comment on lines +306 to +313
if include_tags is None and include_test_cases is None:
click.echo("❌ No config values provided")
click.echo(
"💡 Use --include-tags/--no-include-tags and/or "
"--include-test-cases/--no-include-test-cases"
)
return
Comment on lines +312 to +320

config_data = config_manager.load_config()

if include_tags is not None:
config_data.export_include_tags = include_tags
if include_test_cases is not None:
config_data.export_include_test_cases = include_test_cases

config_manager.save_config(config_data)

click.echo("✅ Updated project export defaults")
click.echo(f" export_include_tags: {config_data.export_include_tags}")
click.echo(f" export_include_test_cases: {config_data.export_include_test_cases}")


@set_project_config.command(name="show")
@click.option(
"--output-mode",
type=click.Choice(["table", "json"]),
default="table",
help="Output format: table (default) or json",
)
@handle_cli_exceptions
@inject
async def show_project_config(
output_mode: str = "table",
config_manager: ConfigManager = Provide[Container.config_manager],
) -> None:
"""Show project export defaults from local .workatoenv config."""
config_data = config_manager.load_config()

if output_mode == "json":
click.echo(
json.dumps(
{
"export_include_tags": config_data.export_include_tags,
"export_include_test_cases": config_data.export_include_test_cases,
}
)
)
return

click.echo("📋 Project export defaults:")
click.echo(f" export_include_tags: {config_data.export_include_tags}")
click.echo(f" export_include_test_cases: {config_data.export_include_test_cases}")


async def _get_local_projects(
config_manager: ConfigManager,
) -> list[tuple[Any, str, ConfigData | None]]:
Expand Down
69 changes: 69 additions & 0 deletions src/workato_platform_cli/cli/commands/projects/project_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,80 @@ async def check_folder_assets(self, folder_id: int) -> list[Asset]:
)
return assets

@staticmethod
def _parse_bool_value(value: str | None) -> bool | None:
"""Parse common string boolean formats to bool."""
if value is None:
return None

normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "y", "on"}:
return True
if normalized in {"0", "false", "no", "n", "off"}:
return False
return None

def _resolve_export_manifest_flags(
self,
include_tags: bool | None,
include_test_cases: bool | None,
) -> tuple[bool, bool]:
"""Resolve export manifest flags."""
config_include_tags: bool | None = None
config_include_test_cases: bool | None = None

try:
from workato_platform_cli.cli.utils.config import ConfigManager

config_data = ConfigManager(skip_validation=True).load_config()
config_include_tags = config_data.export_include_tags
config_include_test_cases = config_data.export_include_test_cases
except Exception:
# Export should still work even if local config cannot be loaded.
click.echo("⚠️ Could not load config, using defaults")
pass
Comment on lines +135 to +138

env_include_tags = self._parse_bool_value(
os.environ.get("WORKATO_INCLUDE_TAGS")
)
env_include_test_cases = self._parse_bool_value(
os.environ.get("WORKATO_INCLUDE_TEST_CASES")
)

resolved_include_tags = (
include_tags
if include_tags is not None
else config_include_tags
if config_include_tags is not None
else env_include_tags
if env_include_tags is not None
else False
)
resolved_include_test_cases = (
include_test_cases
if include_test_cases is not None
else config_include_test_cases
if config_include_test_cases is not None
else env_include_test_cases
if env_include_test_cases is not None
else False
)

return resolved_include_tags, resolved_include_test_cases

async def export_project(
self,
folder_id: int,
project_name: str,
target_dir: str = "project",
include_tags: bool | None = None,
include_test_cases: bool | None = None,
) -> str | None:
"""Export project assets and return project directory path"""
resolved_include_tags, resolved_include_test_cases = (
self._resolve_export_manifest_flags(include_tags, include_test_cases)
)

# Check if project has any assets before attempting export
assets = await self.check_folder_assets(folder_id)

Expand All @@ -131,6 +198,8 @@ async def export_project(
name=project_name,
folder_id=folder_id,
auto_generate_assets=True,
include_tags=resolved_include_tags,
include_test_cases=resolved_include_test_cases,
)
)
)
Expand Down
10 changes: 10 additions & 0 deletions src/workato_platform_cli/cli/utils/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ class ConfigData(BaseModel):
)
folder_id: int | None = Field(None, description="Folder ID")
profile: str | None = Field(None, description="Profile override")
export_include_tags: bool | None = Field(
None,
description="Default value for include_tags when exporting project manifests",
)
export_include_test_cases: bool | None = Field(
None,
description=(
"Default value for include_test_cases when exporting project manifests"
),
)


class RegionInfo(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class ApiClient(BaseModel):
mtls_enabled: Optional[StrictBool] = None
validation_formula: Optional[StrictStr] = None
cert_bundle_ids: Optional[List[StrictInt]] = None
api_policies: Optional[List[ApiClientApiPoliciesInner]] = Field(default=None, description="List of API policies associated with the client")
api_collections: Optional[List[ApiClientApiCollectionsInner]] = Field(default=None, description="List of API collections associated with the client")
api_policies: List[ApiClientApiPoliciesInner] = Field(description="List of API policies associated with the client")
api_collections: List[ApiClientApiCollectionsInner] = Field(description="List of API collections associated with the client")
Comment on lines +48 to +49
__properties: ClassVar[List[str]] = ["id", "name", "description", "active_api_keys_count", "total_api_keys_count", "created_at", "updated_at", "logo", "logo_2x", "is_legacy", "email", "auth_type", "api_token", "mtls_enabled", "validation_formula", "cert_bundle_ids", "api_policies", "api_collections"]

@field_validator('auth_type')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
import json

from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator
from typing import Any, ClassVar, Dict, List, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
Comment on lines +21 to +22
from workato_platform_cli.client.workato_api.models.import_results import ImportResults
from typing import Optional, Set
Comment on lines +22 to 24
from typing_extensions import Self
Expand All @@ -30,7 +30,7 @@ class ApiCollection(BaseModel):
""" # noqa: E501
id: StrictInt
name: StrictStr
project_id: Optional[Union[StrictStr, StrictInt]] = None
project_id: StrictStr
url: StrictStr
api_spec_url: StrictStr
version: StrictStr
Expand All @@ -40,14 +40,6 @@ class ApiCollection(BaseModel):
import_results: Optional[ImportResults] = None
__properties: ClassVar[List[str]] = ["id", "name", "project_id", "url", "api_spec_url", "version", "created_at", "updated_at", "message", "import_results"]

@field_validator('project_id', mode='before')
@classmethod
def coerce_project_id_to_string(cls, v):
"""Coerce project_id to string since API may return int or string"""
if v is not None:
return str(v)
return v

model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
Expand Down
83 changes: 83 additions & 0 deletions tests/unit/commands/projects/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,3 +1138,86 @@ async def mock_aexit(_self: Any, *_args: Any) -> None:
mock_create_config.assert_called_once_with(
config_manager=config_manager, cli_profile="test-profile"
)


@pytest.mark.asyncio
async def test_projects_config_sets_include_defaults(capture_echo: list[str]) -> None:
"""Projects config command should persist include defaults."""
config_manager = Mock()
config_data = ConfigData()
config_manager.load_config.return_value = config_data
config_manager.save_config = Mock()

await command.set_project_config.callback( # type: ignore[misc]
# ctx=Mock(invoked_subcommand=None),
include_tags=True,
include_test_cases=False,
config_manager=config_manager,
)

saved_config = config_manager.save_config.call_args.args[0]
assert saved_config.export_include_tags is True
assert saved_config.export_include_test_cases is False
output = "\n".join(capture_echo)
assert "Updated project export defaults" in output


@pytest.mark.asyncio
async def test_projects_config_requires_at_least_one_value(
capture_echo: list[str],
) -> None:
"""Projects config command should reject empty updates."""
config_manager = Mock()
config_manager.load_config = Mock()
config_manager.save_config = Mock()

await command.set_project_config.callback( # type: ignore[misc]
# ctx=Mock(invoked_subcommand=None),
include_tags=None,
include_test_cases=None,
config_manager=config_manager,
)

config_manager.load_config.assert_not_called()
config_manager.save_config.assert_not_called()
output = "\n".join(capture_echo)
assert "No config values provided" in output


@pytest.mark.asyncio
async def test_projects_config_show_outputs_table(capture_echo: list[str]) -> None:
"""Projects config show should print table output by default."""
config_manager = Mock()
config_manager.load_config.return_value = ConfigData(
export_include_tags=True,
export_include_test_cases=False,
)

await command.show_project_config.callback( # type: ignore[misc]
output_mode="table",
config_manager=config_manager,
)

output = "\n".join(capture_echo)
assert "Project export defaults" in output
assert "export_include_tags: True" in output
assert "export_include_test_cases: False" in output


@pytest.mark.asyncio
async def test_projects_config_show_outputs_json(capture_echo: list[str]) -> None:
"""Projects config show should support JSON output."""
config_manager = Mock()
config_manager.load_config.return_value = ConfigData(
export_include_tags=False,
export_include_test_cases=True,
)

await command.show_project_config.callback( # type: ignore[misc]
output_mode="json",
config_manager=config_manager,
)

data = json.loads("".join(capture_echo))
assert data["export_include_tags"] is False
assert data["export_include_test_cases"] is True
Loading