From 9c2cd7706aa9e7beb84ce88786f4a94deefc8e2c Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:12:00 +0300 Subject: [PATCH 1/8] remove unnecessary comments --- src/lazy_ecs/__init__.py | 14 +++++--------- src/lazy_ecs/features/service/service.py | 2 -- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/lazy_ecs/__init__.py b/src/lazy_ecs/__init__.py index 49b7edf..2ec9f63 100644 --- a/src/lazy_ecs/__init__.py +++ b/src/lazy_ecs/__init__.py @@ -46,13 +46,9 @@ def main() -> None: def _create_aws_client(profile_name: str | None) -> "ECSClient": """Create optimized AWS ECS client with connection pooling.""" - # Optimized configuration for better performance config = Config( - max_pool_connections=5, # Increase from default 1, but keep reasonable for CLI - retries={ - "max_attempts": 2, # Reduce from default 3 for faster failure - "mode": "adaptive", - }, + max_pool_connections=5, + retries={"max_attempts": 2, "mode": "adaptive"}, ) if profile_name: @@ -64,7 +60,7 @@ def _create_aws_client(profile_name: str | None) -> "ECSClient": def _create_logs_client(profile_name: str | None) -> "CloudWatchLogsClient": """Create optimized CloudWatch Logs client with connection pooling.""" config = Config( - max_pool_connections=5, # Same config as ECS client + max_pool_connections=5, retries={"max_attempts": 2, "mode": "adaptive"}, ) @@ -77,7 +73,7 @@ def _create_logs_client(profile_name: str | None) -> "CloudWatchLogsClient": def _create_sts_client(profile_name: str | None) -> "STSClient": """Create optimized STS client with connection pooling.""" config = Config( - max_pool_connections=5, # Same config as ECS client + max_pool_connections=5, retries={"max_attempts": 2, "mode": "adaptive"}, ) @@ -90,7 +86,7 @@ def _create_sts_client(profile_name: str | None) -> "STSClient": def _create_cloudwatch_client(profile_name: str | None) -> "CloudWatchClient": """Create optimized CloudWatch client with connection pooling.""" config = Config( - max_pool_connections=5, # Same config as ECS client + max_pool_connections=5, retries={"max_attempts": 2, "mode": "adaptive"}, ) diff --git a/src/lazy_ecs/features/service/service.py b/src/lazy_ecs/features/service/service.py index b9877db..24d3e76 100644 --- a/src/lazy_ecs/features/service/service.py +++ b/src/lazy_ecs/features/service/service.py @@ -55,7 +55,6 @@ def get_service_events(self, cluster_name: str, service_name: str) -> list[Servi events = services[0].get("events", []) service_events = [_create_service_event(dict(event)) for event in events] - # Sort by creation time, most recent first (handle None values) return sorted(service_events, key=lambda x: x["created_at"] or datetime.min, reverse=True) @@ -99,7 +98,6 @@ def _categorize_event(message: str) -> str: """Categorize service event based on message content.""" message_lower = message.lower() - # Check failure first to catch "deployment failed" as failure, not deployment if any(term in message_lower for term in ["failed", "error", "unhealthy", "unable"]): return "failure" if any( From 21db593eecbb291c231efdb554d125599b5254ce Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:12:53 +0300 Subject: [PATCH 2/8] more pythonic style --- src/lazy_ecs/__init__.py | 24 +++++++------------- src/lazy_ecs/features/container/container.py | 4 +--- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/lazy_ecs/__init__.py b/src/lazy_ecs/__init__.py index 2ec9f63..de75d89 100644 --- a/src/lazy_ecs/__init__.py +++ b/src/lazy_ecs/__init__.py @@ -51,10 +51,8 @@ def _create_aws_client(profile_name: str | None) -> "ECSClient": retries={"max_attempts": 2, "mode": "adaptive"}, ) - if profile_name: - session = boto3.Session(profile_name=profile_name) - return session.client("ecs", config=config) - return boto3.client("ecs", config=config) + session = boto3.Session(profile_name=profile_name) if profile_name else boto3 + return session.client("ecs", config=config) def _create_logs_client(profile_name: str | None) -> "CloudWatchLogsClient": @@ -64,10 +62,8 @@ def _create_logs_client(profile_name: str | None) -> "CloudWatchLogsClient": retries={"max_attempts": 2, "mode": "adaptive"}, ) - if profile_name: - session = boto3.Session(profile_name=profile_name) - return session.client("logs", config=config) - return boto3.client("logs", config=config) + session = boto3.Session(profile_name=profile_name) if profile_name else boto3 + return session.client("logs", config=config) def _create_sts_client(profile_name: str | None) -> "STSClient": @@ -77,10 +73,8 @@ def _create_sts_client(profile_name: str | None) -> "STSClient": retries={"max_attempts": 2, "mode": "adaptive"}, ) - if profile_name: - session = boto3.Session(profile_name=profile_name) - return session.client("sts", config=config) - return boto3.client("sts", config=config) + session = boto3.Session(profile_name=profile_name) if profile_name else boto3 + return session.client("sts", config=config) def _create_cloudwatch_client(profile_name: str | None) -> "CloudWatchClient": @@ -90,10 +84,8 @@ def _create_cloudwatch_client(profile_name: str | None) -> "CloudWatchClient": retries={"max_attempts": 2, "mode": "adaptive"}, ) - if profile_name: - session = boto3.Session(profile_name=profile_name) - return session.client("cloudwatch", config=config) - return boto3.client("cloudwatch", config=config) + session = boto3.Session(profile_name=profile_name) if profile_name else boto3 + return session.client("cloudwatch", config=config) def _navigate_clusters(navigator: ECSNavigator, ecs_service: ECSService) -> None: diff --git a/src/lazy_ecs/features/container/container.py b/src/lazy_ecs/features/container/container.py index a4cf19e..899e4ec 100644 --- a/src/lazy_ecs/features/container/container.py +++ b/src/lazy_ecs/features/container/container.py @@ -130,9 +130,7 @@ def get_live_container_logs_tail( return region = self.ecs_client.meta.region_name aws_account_id = ( - (lambda: self.sts_client.get_caller_identity().get("Account"))() - if self.sts_client - else environ.get("AWS_ACCOUNT_ID") + self.sts_client.get_caller_identity().get("Account") if self.sts_client else environ.get("AWS_ACCOUNT_ID") ) if not region or not aws_account_id: return From 63526886fa7b0f435ef1698b93cc2e79323d9f02 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:13:58 +0300 Subject: [PATCH 3/8] remove unnecessary function --- src/lazy_ecs/features/service/ui.py | 13 ++----------- tests/test_service_ui.py | 11 +---------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/lazy_ecs/features/service/ui.py b/src/lazy_ecs/features/service/ui.py index 4a110ec..39a33b3 100644 --- a/src/lazy_ecs/features/service/ui.py +++ b/src/lazy_ecs/features/service/ui.py @@ -87,7 +87,8 @@ def display_service_events(self, cluster_name: str, service_name: str) -> None: time_str = created_at.strftime("%m/%d %H:%M") if created_at else "Unknown" event_type = event["event_type"] - type_style = _get_event_type_style(event_type) + type_styles = {"deployment": "blue", "scaling": "yellow", "failure": "red", "other": "white"} + type_style = type_styles.get(event_type, "white") type_display = f"[{type_style}]{event_type.title()}[/{type_style}]" message = event["message"] @@ -118,13 +119,3 @@ def display_service_metrics(self, service_name: str, metrics: ServiceMetrics) -> console.print(f"\n[bold cyan]Metrics for service '{service_name}' (last hour):[/bold cyan]") for line in lines: console.print(line) - - -def _get_event_type_style(event_type: str) -> str: - event_styles = { - "deployment": "blue", - "scaling": "yellow", - "failure": "red", - "other": "white", - } - return event_styles.get(event_type, "white") diff --git a/tests/test_service_ui.py b/tests/test_service_ui.py index 24bcbc6..11c4aa5 100644 --- a/tests/test_service_ui.py +++ b/tests/test_service_ui.py @@ -7,7 +7,7 @@ from lazy_ecs.features.service.actions import ServiceActions from lazy_ecs.features.service.service import ServiceService -from lazy_ecs.features.service.ui import ServiceUI, _get_event_type_style +from lazy_ecs.features.service.ui import ServiceUI @pytest.fixture @@ -207,15 +207,6 @@ def test_display_service_events_no_events(mock_print, service_ui): mock_print.assert_called_once_with("No events found for service 'web-api'", style="blue") -def test_get_event_type_style(): - """Test event type style mapping.""" - assert _get_event_type_style("deployment") == "blue" - assert _get_event_type_style("scaling") == "yellow" - assert _get_event_type_style("failure") == "red" - assert _get_event_type_style("other") == "white" - assert _get_event_type_style("unknown") == "white" # Default case - - @patch("lazy_ecs.features.service.ui.console.print") def test_service_name_truncation_shows_end(mock_print, service_ui): """Test that long service names are truncated to show the distinguishing end part.""" From 42dc3be48b87dc558e9c7705d631934ad756bf2e Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:14:27 +0300 Subject: [PATCH 4/8] remove dead code --- src/lazy_ecs/features/task/ui.py | 51 +------------------------------- src/lazy_ecs/ui.py | 30 ------------------- tests/test_task_history_ui.py | 13 -------- tests/test_ui.py | 38 ------------------------ 4 files changed, 1 insertion(+), 131 deletions(-) diff --git a/src/lazy_ecs/features/task/ui.py b/src/lazy_ecs/features/task/ui.py index 7214999..ab16f1c 100644 --- a/src/lazy_ecs/features/task/ui.py +++ b/src/lazy_ecs/features/task/ui.py @@ -9,7 +9,7 @@ from rich.table import Table from ...core.base import BaseUIComponent -from ...core.navigation import add_navigation_choices, select_with_auto_pagination +from ...core.navigation import select_with_auto_pagination from ...core.types import TaskDetails, TaskHistoryDetails from ...core.utils import print_warning, show_spinner from .comparison import TaskComparisonService, compare_task_definitions @@ -390,52 +390,3 @@ def _format_volumes(volumes: list[dict[str, Any]]) -> str: f"{vol.get('sourceVolume', '?')}:{vol.get('containerPath', '?')}{':ro' if vol.get('readOnly') else ''}" for vol in volumes ) - - -def _build_task_feature_choices(containers: list[dict[str, Any]]) -> list[dict[str, str]]: - choices = [] - - # Add task-level features first - show task details as first option - choices.extend( - [ - { - "name": "Show task details", - "value": "task_action:show_details", - }, - { - "name": "Show task history and failures", - "value": "task_action:show_history", - }, - ] - ) - - for container in containers: - container_name = container["name"] - - # Add container features - choices.extend( - [ - { - "name": f"Show logs (tail) for container '{container_name}'", - "value": f"container_action:tail_logs:{container_name}", - }, - { - "name": f"Show environment variables for '{container_name}'", - "value": f"container_action:show_env:{container_name}", - }, - { - "name": f"Show secrets for '{container_name}'", - "value": f"container_action:show_secrets:{container_name}", - }, - { - "name": f"Show port mappings for '{container_name}'", - "value": f"container_action:show_ports:{container_name}", - }, - { - "name": f"Show volume mounts for '{container_name}'", - "value": f"container_action:show_volumes:{container_name}", - }, - ] - ) - - return add_navigation_choices(choices, "Back to service selection") diff --git a/src/lazy_ecs/ui.py b/src/lazy_ecs/ui.py index d490304..cd5b918 100644 --- a/src/lazy_ecs/ui.py +++ b/src/lazy_ecs/ui.py @@ -2,13 +2,10 @@ from __future__ import annotations -from typing import Any - from rich.console import Console from .aws_service import ECSService from .core.base import BaseUIComponent -from .core.navigation import add_navigation_choices from .core.types import TaskDetails from .core.utils import show_spinner from .features.cluster.cluster import ClusterService @@ -141,30 +138,3 @@ def open_task_in_console(self, cluster_name: str, task_arn: str) -> None: url = build_task_url(region, cluster_name, task_arn) console.print(f"\n🌐 Opening task in AWS console: {url}", style="cyan") webbrowser.open(url) - - -def _build_task_feature_choices(containers: list[dict[str, Any]]) -> list[dict[str, str]]: - """Build feature menu choices for containers plus navigation options.""" - actions = [ - ("Show logs (tail) for container: {name}", "container_action", "tail_logs"), - ("Show environment variables for container: {name}", "container_action", "show_env"), - ("Show secrets for container: {name}", "container_action", "show_secrets"), - ("Show port mappings for container: {name}", "container_action", "show_ports"), - ("Show volume mounts for container: {name}", "container_action", "show_volumes"), - ] - - choices = [] - - # Add container actions - for display_template, action_type, action_name in actions: - for container in containers: - container_name = container["name"] - choices.append( - { - "name": display_template.format(name=container_name), - "value": f"{action_type}:{action_name}:{container_name}", - } - ) - - # Add navigation options - return add_navigation_choices(choices, "Back to service selection") diff --git a/tests/test_task_history_ui.py b/tests/test_task_history_ui.py index 2a14976..a7e682d 100644 --- a/tests/test_task_history_ui.py +++ b/tests/test_task_history_ui.py @@ -138,16 +138,3 @@ def test_display_failure_analysis(self, mock_print, task_ui, mock_task_service): mock_task_service.get_task_failure_analysis.assert_called_once_with(failed_task) assert any("Failure Analysis" in str(call) for call in mock_print.call_args_list) assert any("memory" in str(call) for call in mock_print.call_args_list) - - def test_build_task_history_choices_includes_history_option(self): - """Test that task feature choices include task history option.""" - # Import the function directly to test it - from lazy_ecs.features.task.ui import _build_task_feature_choices - - containers = [{"name": "web-api", "image": "nginx:latest"}] - choices = _build_task_feature_choices(containers) - - # Check that task history option is included - history_choices = [c for c in choices if "task history" in c["name"].lower()] - assert len(history_choices) > 0 - assert any("task history" in choice["name"].lower() for choice in history_choices) diff --git a/tests/test_ui.py b/tests/test_ui.py index 61f469b..ec13219 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -177,41 +177,3 @@ def test_handle_force_deployment_delegates_to_service_ui(mock_ecs_service) -> No navigator.handle_force_deployment("cluster", "service") navigator._service_ui.handle_force_deployment.assert_called_once_with("cluster", "service") - - -def test_build_task_feature_choices() -> None: - """Test the build_task_feature_choices utility function.""" - from lazy_ecs.ui import _build_task_feature_choices - - containers = [{"name": "web"}, {"name": "sidecar"}] - choices = _build_task_feature_choices(containers) - - # Verify structure - choice_names = [choice["name"] for choice in choices] - choice_values = [choice["value"] for choice in choices] - - # Check container actions are present - assert "Show logs (tail) for container: web" in choice_names - assert "Show environment variables for container: web" in choice_names - assert "Show secrets for container: web" in choice_names - assert "Show port mappings for container: web" in choice_names - assert "Show volume mounts for container: web" in choice_names - - assert "Show logs (tail) for container: sidecar" in choice_names - assert "Show environment variables for container: sidecar" in choice_names - assert "Show secrets for container: sidecar" in choice_names - assert "Show port mappings for container: sidecar" in choice_names - assert "Show volume mounts for container: sidecar" in choice_names - - # Check navigation - assert "ā¬…ļø Back to service selection" in choice_names - assert "āŒ Exit" in choice_names - - # Check values - assert "container_action:tail_logs:web" in choice_values - assert "container_action:show_env:web" in choice_values - assert "navigation:back" in choice_values - assert "navigation:exit" in choice_values - - # Total: 5 actions x 2 containers + 2 navigation = 12 - assert len(choices) == 12 From 91c1e1b13a599e9a002e20974c8aecd2a149466d Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:21:06 +0300 Subject: [PATCH 5/8] extract the change type display configuration --- src/lazy_ecs/__init__.py | 56 ++++++++++------- src/lazy_ecs/features/task/ui.py | 103 ++++++++++++++++--------------- 2 files changed, 85 insertions(+), 74 deletions(-) diff --git a/src/lazy_ecs/__init__.py b/src/lazy_ecs/__init__.py index de75d89..41f62dd 100644 --- a/src/lazy_ecs/__init__.py +++ b/src/lazy_ecs/__init__.py @@ -156,6 +156,32 @@ def _navigate_services(navigator: ECSNavigator, ecs_service: ECSService, cluster # Continue the loop to show the menu again +_CONTAINER_ACTIONS = { + "tail_logs": lambda nav, cluster, task_arn, container: nav.show_container_logs_live_tail( + cluster, task_arn, container + ), + "show_env": lambda nav, cluster, task_arn, container: nav.show_container_environment_variables( + cluster, task_arn, container + ), + "show_secrets": lambda nav, cluster, task_arn, container: nav.show_container_secrets(cluster, task_arn, container), + "show_ports": lambda nav, cluster, task_arn, container: nav.show_container_port_mappings( + cluster, task_arn, container + ), + "show_volumes": lambda nav, cluster, task_arn, container: nav.show_container_volume_mounts( + cluster, task_arn, container + ), +} + +_TASK_ACTIONS = { + "show_history": lambda nav, cluster, service, _task_arn, _task_details: nav.show_task_history(cluster, service), + "show_details": lambda nav, _cluster, _service, _task_arn, task_details: nav.display_task_details(task_details), + "compare_definitions": lambda nav, _cluster, _service, _task_arn, task_details: nav.show_task_definition_comparison( + task_details + ), + "open_console": lambda nav, cluster, _service, task_arn, _task_details: nav.open_task_in_console(cluster, task_arn), +} + + def _handle_task_features( navigator: ECSNavigator, cluster_name: str, task_arn: str, task_details: TaskDetails | None, service_name: str ) -> bool: @@ -163,34 +189,16 @@ def _handle_task_features( while True: selection = navigator.select_task_feature(task_details) - # Handle navigation responses should_continue, should_exit = handle_navigation(selection) if not should_continue: - return not should_exit # True for back, False for exit + return not should_exit selection_type, action_name, container_name = parse_selection(selection) - if selection_type == "container_action": - # Map action names to methods - action_methods = { - "tail_logs": navigator.show_container_logs_live_tail, - "show_env": navigator.show_container_environment_variables, - "show_secrets": navigator.show_container_secrets, - "show_ports": navigator.show_container_port_mappings, - "show_volumes": navigator.show_container_volume_mounts, - } - - if action_name in action_methods: - action_methods[action_name](cluster_name, task_arn, container_name) - - elif selection_type == "task_action": - if action_name == "show_history": - navigator.show_task_history(cluster_name, service_name) - elif action_name == "show_details": - navigator.display_task_details(task_details) - elif action_name == "compare_definitions": - navigator.show_task_definition_comparison(task_details) - elif action_name == "open_console": - navigator.open_task_in_console(cluster_name, task_arn) + + if selection_type == "container_action" and action_name in _CONTAINER_ACTIONS: + _CONTAINER_ACTIONS[action_name](navigator, cluster_name, task_arn, container_name) + elif selection_type == "task_action" and action_name in _TASK_ACTIONS: + _TASK_ACTIONS[action_name](navigator, cluster_name, service_name, task_arn, task_details) if __name__ == "__main__": diff --git a/src/lazy_ecs/features/task/ui.py b/src/lazy_ecs/features/task/ui.py index ab16f1c..6b7de48 100644 --- a/src/lazy_ecs/features/task/ui.py +++ b/src/lazy_ecs/features/task/ui.py @@ -22,6 +22,22 @@ MAX_STATUS_DETAILS_LENGTH = 50 SEPARATOR_WIDTH = 80 +_CHANGE_TYPE_DISPLAY = { + "image_changed": ("🐳", "Image changed for '{container}'"), + "env_added": ("+", "Environment variable added ({container})"), + "env_removed": ("-", "Environment variable removed ({container})"), + "env_changed": ("šŸ”„", "Environment variable changed ({container})"), + "secret_changed": ("šŸ”", "Secret reference changed ({container})"), + "task_cpu_changed": ("šŸ’»", "Task CPU changed"), + "task_memory_changed": ("🧠", "Task Memory changed"), + "container_cpu_changed": ("šŸ’»", "Container CPU changed ({container})"), + "container_memory_changed": ("🧠", "Container Memory changed ({container})"), + "ports_changed": ("šŸ”Œ", "Port mappings changed ({container})"), + "command_changed": ("āš™ļø ", "Command changed ({container})"), + "entrypoint_changed": ("🚪", "Entrypoint changed ({container})"), + "volumes_changed": ("šŸ’¾", "Volume mounts changed ({container})"), +} + class TaskUI(BaseUIComponent): """UI component for task selection and display.""" @@ -319,56 +335,43 @@ def _display_change(self, change: dict[str, Any]) -> None: change_type = change["type"] container = change.get("container", "") - display_config = { - "image_changed": ("🐳", f"Image changed for '{container}'"), - "env_added": ("+", f"Environment variable added ({container})"), - "env_removed": ("-", f"Environment variable removed ({container})"), - "env_changed": ("šŸ”„", f"Environment variable changed ({container})"), - "secret_changed": ("šŸ”", f"Secret reference changed ({container})"), - "task_cpu_changed": ("šŸ’»", "Task CPU changed"), - "task_memory_changed": ("🧠", "Task Memory changed"), - "container_cpu_changed": ("šŸ’»", f"Container CPU changed ({container})"), - "container_memory_changed": ("🧠", f"Container Memory changed ({container})"), - "ports_changed": ("šŸ”Œ", f"Port mappings changed ({container})"), - "command_changed": ("āš™ļø ", f"Command changed ({container})"), - "entrypoint_changed": ("🚪", f"Entrypoint changed ({container})"), - "volumes_changed": ("šŸ’¾", f"Volume mounts changed ({container})"), - } - - if change_type in display_config: - emoji, label = display_config[change_type] - console.print(f"{emoji} {label}:", style="bold yellow") - - if "key" in change: - if change_type.endswith("_added"): - console.print(f" + {change['key']}={change['value']}", style="green") - elif change_type.endswith("_removed"): - console.print(f" - {change['key']}={change['value']}", style="red") - elif change_type.endswith("_changed"): - if change_type == "secret_changed": - console.print(f" {change['key']}: ARN updated", style="yellow") - else: - console.print(f" {change['key']}:", style="white") - console.print(f" - {change['old']}", style="red") - console.print(f" + {change['new']}", style="green") - elif change_type == "ports_changed": - if change.get("old"): - console.print(f" - {_format_ports(change['old'])}", style="red") - if change.get("new"): - console.print(f" + {_format_ports(change['new'])}", style="green") - elif change_type in ("command_changed", "entrypoint_changed"): - if change.get("old"): - console.print(f" - {' '.join(change['old'])}", style="red") - if change.get("new"): - console.print(f" + {' '.join(change['new'])}", style="green") - elif change_type == "volumes_changed": - if change.get("old"): - console.print(f" - {_format_volumes(change['old'])}", style="red") - if change.get("new"): - console.print(f" + {_format_volumes(change['new'])}", style="green") - else: - console.print(f" - {change.get('old')}", style="red") - console.print(f" + {change.get('new')}", style="green") + if change_type not in _CHANGE_TYPE_DISPLAY: + return + + emoji, label_template = _CHANGE_TYPE_DISPLAY[change_type] + label = label_template.format(container=container) if "{container}" in label_template else label_template + console.print(f"{emoji} {label}:", style="bold yellow") + + if "key" in change: + if change_type.endswith("_added"): + console.print(f" + {change['key']}={change['value']}", style="green") + elif change_type.endswith("_removed"): + console.print(f" - {change['key']}={change['value']}", style="red") + elif change_type.endswith("_changed"): + if change_type == "secret_changed": + console.print(f" {change['key']}: ARN updated", style="yellow") + else: + console.print(f" {change['key']}:", style="white") + console.print(f" - {change['old']}", style="red") + console.print(f" + {change['new']}", style="green") + elif change_type == "ports_changed": + if change.get("old"): + console.print(f" - {_format_ports(change['old'])}", style="red") + if change.get("new"): + console.print(f" + {_format_ports(change['new'])}", style="green") + elif change_type in ("command_changed", "entrypoint_changed"): + if change.get("old"): + console.print(f" - {' '.join(change['old'])}", style="red") + if change.get("new"): + console.print(f" + {' '.join(change['new'])}", style="green") + elif change_type == "volumes_changed": + if change.get("old"): + console.print(f" - {_format_volumes(change['old'])}", style="red") + if change.get("new"): + console.print(f" + {_format_volumes(change['new'])}", style="green") + else: + console.print(f" - {change.get('old')}", style="red") + console.print(f" + {change.get('new')}", style="green") def _format_ports(ports: list[dict[str, Any]]) -> str: From ba1fbe0f3f70ccdb82d14b9a455a96a7bdffa2d3 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:23:53 +0300 Subject: [PATCH 6/8] improve naming --- src/lazy_ecs/features/service/service.py | 6 +++--- src/lazy_ecs/features/task/task.py | 8 ++++---- tests/test_service_events.py | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lazy_ecs/features/service/service.py b/src/lazy_ecs/features/service/service.py index 24d3e76..78d554d 100644 --- a/src/lazy_ecs/features/service/service.py +++ b/src/lazy_ecs/features/service/service.py @@ -53,7 +53,7 @@ def get_service_events(self, cluster_name: str, service_name: str) -> list[Servi return [] events = services[0].get("events", []) - service_events = [_create_service_event(dict(event)) for event in events] + service_events = [_parse_service_event(dict(event)) for event in events] return sorted(service_events, key=lambda x: x["created_at"] or datetime.min, reverse=True) @@ -78,8 +78,8 @@ def _create_service_info(service: ServiceTypeDef) -> ServiceInfo: } -def _create_service_event(event: dict[str, Any]) -> ServiceEvent: - """Create service event from AWS event description.""" +def _parse_service_event(event: dict[str, Any]) -> ServiceEvent: + """Parse service event from AWS event description.""" event_id = event.get("id", "") created_at = event.get("createdAt") message = event.get("message", "") diff --git a/src/lazy_ecs/features/task/task.py b/src/lazy_ecs/features/task/task.py index eb39d7b..4dadd3a 100644 --- a/src/lazy_ecs/features/task/task.py +++ b/src/lazy_ecs/features/task/task.py @@ -63,8 +63,8 @@ def get_task_and_definition( return task, task_definition - def _list_tasks_paginated(self, cluster_name: str, service_name: str | None, desired_status: str) -> list[str]: - """List tasks with optional service name filtering.""" + def _list_tasks_by_status(self, cluster_name: str, service_name: str | None, desired_status: str) -> list[str]: + """List tasks filtered by status and optional service name.""" kwargs = {"cluster": cluster_name, "desiredStatus": desired_status} if service_name: kwargs["serviceName"] = service_name @@ -74,10 +74,10 @@ def get_task_history(self, cluster_name: str, service_name: str | None = None) - """Get task history including stopped tasks with failure information.""" task_arns = [] - running_arns = self._list_tasks_paginated(cluster_name, service_name, "RUNNING") + running_arns = self._list_tasks_by_status(cluster_name, service_name, "RUNNING") task_arns.extend(running_arns) - stopped_arns = self._list_tasks_paginated(cluster_name, service_name, "STOPPED") + stopped_arns = self._list_tasks_by_status(cluster_name, service_name, "STOPPED") task_arns.extend(stopped_arns) if not task_arns: diff --git a/tests/test_service_events.py b/tests/test_service_events.py index e482a7b..b37bc3c 100644 --- a/tests/test_service_events.py +++ b/tests/test_service_events.py @@ -2,7 +2,7 @@ from datetime import datetime -from lazy_ecs.features.service.service import _categorize_event, _create_service_event +from lazy_ecs.features.service.service import _categorize_event, _parse_service_event def test_categorize_event_deployment(): @@ -62,8 +62,8 @@ def test_categorize_event_other(): assert _categorize_event(message) == "other" -def test_create_service_event(): - """Test service event creation from AWS event data.""" +def test_parse_service_event(): + """Test service event parsing from AWS event data.""" test_time = datetime(2024, 1, 15, 10, 30, 45) aws_event = { "id": "event-123", @@ -71,7 +71,7 @@ def test_create_service_event(): "message": "has started a deployment", } - event = _create_service_event(aws_event) + event = _parse_service_event(aws_event) assert event["id"] == "event-123" assert event["created_at"] == test_time @@ -79,11 +79,11 @@ def test_create_service_event(): assert event["event_type"] == "deployment" -def test_create_service_event_minimal(): - """Test service event creation with minimal AWS data.""" +def test_parse_service_event_minimal(): + """Test service event parsing with minimal AWS data.""" aws_event = {} - event = _create_service_event(aws_event) + event = _parse_service_event(aws_event) assert event["id"] == "" assert event["created_at"] is None @@ -91,13 +91,13 @@ def test_create_service_event_minimal(): assert event["event_type"] == "other" -def test_create_service_event_missing_fields(): - """Test service event creation with some missing fields.""" +def test_parse_service_event_missing_fields(): + """Test service event parsing with some missing fields.""" aws_event = { "message": "task failed to start due to resource constraints", } - event = _create_service_event(aws_event) + event = _parse_service_event(aws_event) assert event["id"] == "" assert event["created_at"] is None From d8aa458a247dd26cafeb5d957e3a6c1db5b823dd Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:39:20 +0300 Subject: [PATCH 7/8] Enabled ~200+ additional ruff rules - Auto-fixed 120 violations - Added 20 sensible rule ignores --- pyproject.toml | 75 +++++++++++++++----- scripts/generate_screenshots.py | 6 +- src/lazy_ecs/__init__.py | 24 +++++-- src/lazy_ecs/aws_service.py | 44 +++++++++--- src/lazy_ecs/core/navigation.py | 24 +++++-- src/lazy_ecs/core/types.py | 5 +- src/lazy_ecs/core/utils.py | 2 +- src/lazy_ecs/features/container/container.py | 20 ++++-- src/lazy_ecs/features/container/ui.py | 17 +++-- src/lazy_ecs/features/service/ui.py | 6 +- src/lazy_ecs/features/task/comparison.py | 20 ++++-- src/lazy_ecs/features/task/task.py | 27 +++++-- src/lazy_ecs/features/task/ui.py | 23 ++++-- tests/conftest.py | 1 + tests/test_aws_service.py | 15 ++-- tests/test_container_ui.py | 59 ++++++++++----- tests/test_service_events.py | 6 +- tests/test_service_metrics.py | 4 +- tests/test_service_ui.py | 12 ++-- tests/test_task_comparison.py | 22 +++--- tests/test_task_comparison_service.py | 6 +- tests/test_task_history.py | 20 +++--- tests/test_task_history_ui.py | 6 +- tests/test_task_ui.py | 2 +- tests/test_ui.py | 3 +- 25 files changed, 313 insertions(+), 136 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0e5ec65..02419e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,28 +55,71 @@ target-version = "py311" line-length = 120 [tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade - "ANN", # flake8-annotations - require type annotations - "N", # naming conventions - "SIM", # simplify - "RET", # return values - "ARG", # unused arguments - "PTH", # pathlib - "RUF", # ruff-specific rules +select = ["ALL"] +ignore = [ + # Formatter conflicts + "COM812", # Trailing comma - conflicts with formatter + "ISC001", # Implicit string concatenation - conflicts with formatter + + # Docstrings - too verbose for this project style + "D100", + "D101", + "D102", + "D103", + "D104", + "D105", + "D107", + "D203", + "D213", # Incompatible docstring rules + "D401", # First line should be imperative - we prefer descriptive + + # Line length - we use 120, not 88 + "E501", # Handle with line-length = 120 instead + + # Not applicable for CLI tool + "TID252", # Relative imports are fine in our structure + "TC001", + "TC002", + "TC003", # Type checking imports - not worth the complexity + + # Too restrictive + "FBT001", # Boolean positional args are OK + "S110", # Try/except pass - sometimes needed + "SLF001", # Private member access - needed for testing + "PLC0415", # Import at top - sometimes needed for dynamic imports + "BLE001", # Blind except - acceptable for top-level error handling + "PLR2004", # Magic values - small numbers in comparisons are clear + "DTZ006", + "DTZ901", # Datetime timezone - not critical for this tool + "PERF401", # List comprehensions - readability over micro-optimizations + "TRY300", + "TRY301", # Try/except else blocks - not needed + + # Complexity - will address selectively + "C901", # Function complexity + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments + "PLR0915", # Too many statements ] -ignore = [] [tool.ruff.lint.per-file-ignores] "tests/**" = [ "ANN001", # Missing type annotation for function argument (test fixtures/mocks) "ANN201", # Missing return type annotation for public function (test fixtures) + "S101", # Use of assert - that's the point of tests! + "PLR0913", # Too many arguments in test functions + "PLR2004", # Magic values in tests are fine for readability + "DTZ001", # Datetime without timezone - test data doesn't need it + "PT019", # Fixture without value - sometimes needed for side effects + "PGH003", # Type ignore without code - acceptable in tests +] +"scripts/**" = [ + "T201", # Print statements - needed for script output + "PLC0415", # Import in function - needed for screenshot generation + "DTZ001", # Datetime without timezone - mock data is fine + "PGH003", # Type ignore without code - will fix if needed + "EXE001", # Shebang on non-executable - handled by uv run ] [tool.pyrefly] diff --git a/scripts/generate_screenshots.py b/scripts/generate_screenshots.py index 64756a0..ef2a129 100644 --- a/scripts/generate_screenshots.py +++ b/scripts/generate_screenshots.py @@ -75,7 +75,7 @@ def generate_task_comparison_screenshot() -> None: "environment": {"ENV": "staging", "DEBUG": "true"}, "cpu": 256, "memory": 512, - } + }, ], "taskCpu": "256", "taskMemory": "512", @@ -91,7 +91,7 @@ def generate_task_comparison_screenshot() -> None: "environment": {"ENV": "production", "LOG_LEVEL": "info"}, "cpu": 256, "memory": 512, - } + }, ], "taskCpu": "512", "taskMemory": "1024", @@ -186,7 +186,7 @@ def generate_task_failure_screenshot() -> None: "reason": "OutOfMemoryError: Java heap space", "health_status": None, "last_status": "STOPPED", - } + }, ], }, { diff --git a/src/lazy_ecs/__init__.py b/src/lazy_ecs/__init__.py index 41f62dd..272d974 100644 --- a/src/lazy_ecs/__init__.py +++ b/src/lazy_ecs/__init__.py @@ -158,17 +158,25 @@ def _navigate_services(navigator: ECSNavigator, ecs_service: ECSService, cluster _CONTAINER_ACTIONS = { "tail_logs": lambda nav, cluster, task_arn, container: nav.show_container_logs_live_tail( - cluster, task_arn, container + cluster, + task_arn, + container, ), "show_env": lambda nav, cluster, task_arn, container: nav.show_container_environment_variables( - cluster, task_arn, container + cluster, + task_arn, + container, ), "show_secrets": lambda nav, cluster, task_arn, container: nav.show_container_secrets(cluster, task_arn, container), "show_ports": lambda nav, cluster, task_arn, container: nav.show_container_port_mappings( - cluster, task_arn, container + cluster, + task_arn, + container, ), "show_volumes": lambda nav, cluster, task_arn, container: nav.show_container_volume_mounts( - cluster, task_arn, container + cluster, + task_arn, + container, ), } @@ -176,14 +184,18 @@ def _navigate_services(navigator: ECSNavigator, ecs_service: ECSService, cluster "show_history": lambda nav, cluster, service, _task_arn, _task_details: nav.show_task_history(cluster, service), "show_details": lambda nav, _cluster, _service, _task_arn, task_details: nav.display_task_details(task_details), "compare_definitions": lambda nav, _cluster, _service, _task_arn, task_details: nav.show_task_definition_comparison( - task_details + task_details, ), "open_console": lambda nav, cluster, _service, task_arn, _task_details: nav.open_task_in_console(cluster, task_arn), } def _handle_task_features( - navigator: ECSNavigator, cluster_name: str, task_arn: str, task_details: TaskDetails | None, service_name: str + navigator: ECSNavigator, + cluster_name: str, + task_arn: str, + task_details: TaskDetails | None, + service_name: str, ) -> bool: """Handle task feature selection and execution. Returns True if back was chosen, False if exit.""" while True: diff --git a/src/lazy_ecs/aws_service.py b/src/lazy_ecs/aws_service.py index 66aafa8..81e3e4c 100644 --- a/src/lazy_ecs/aws_service.py +++ b/src/lazy_ecs/aws_service.py @@ -57,7 +57,10 @@ def get_tasks(self, cluster_name: str, service_name: str) -> list[str]: return self._task.get_tasks(cluster_name, service_name) def _with_desired_task_definition( - self, cluster_name: str, service_name: str, operation: Callable[[str | None], Any] + self, + cluster_name: str, + service_name: str, + operation: Callable[[str | None], Any], ) -> Any: # noqa: ANN401 """Helper to reduce repetition in task operations that need desired task definition.""" desired_task_def_arn = self._service.get_desired_task_definition_arn(cluster_name, service_name) @@ -66,13 +69,17 @@ def _with_desired_task_definition( def get_task_info(self, cluster_name: str, service_name: str) -> list[TaskInfo]: """Get detailed task information with human-readable names.""" return self._with_desired_task_definition( - cluster_name, service_name, lambda arn: self._task.get_task_info(cluster_name, service_name, arn) + cluster_name, + service_name, + lambda arn: self._task.get_task_info(cluster_name, service_name, arn), ) def get_task_details(self, cluster_name: str, service_name: str, task_arn: str) -> TaskDetails | None: """Get comprehensive task details.""" return self._with_desired_task_definition( - cluster_name, service_name, lambda arn: self._task.get_task_details(cluster_name, task_arn, arn) + cluster_name, + service_name, + lambda arn: self._task.get_task_details(cluster_name, task_arn, arn), ) def get_log_config(self, cluster_name: str, task_arn: str, container_name: str) -> LogConfig | None: @@ -84,7 +91,10 @@ def get_container_logs(self, log_group: str, log_stream: str, lines: int = 50) - return self._container.get_container_logs(log_group, log_stream, lines) def get_live_container_logs_tail( - self, log_group: str, log_stream: str, event_filter_pattern: str = "" + self, + log_group: str, + log_stream: str, + event_filter_pattern: str = "", ) -> Generator[StartLiveTailResponseStreamTypeDef | LiveTailSessionLogEventTypeDef]: """Tail container logs in real time from CloudWatch.""" return self._container.get_live_container_logs_tail(log_group, log_stream, event_filter_pattern) @@ -94,7 +104,11 @@ def list_log_groups(self, cluster_name: str, container_name: str) -> list[str]: return self._container.list_log_groups(cluster_name, container_name) def _with_container_context( - self, cluster_name: str, task_arn: str, container_name: str, operation: Callable[[Any], Any] + self, + cluster_name: str, + task_arn: str, + container_name: str, + operation: Callable[[Any], Any], ) -> Any: # noqa: ANN401 """Helper to reduce repetition in container operations.""" context = self._container.get_container_context(cluster_name, task_arn, container_name) @@ -103,11 +117,17 @@ def _with_container_context( return operation(context) def get_container_environment_variables( - self, cluster_name: str, task_arn: str, container_name: str + self, + cluster_name: str, + task_arn: str, + container_name: str, ) -> dict[str, str] | None: """Get environment variables for a specific container in a task.""" return self._with_container_context( - cluster_name, task_arn, container_name, self._container.get_environment_variables + cluster_name, + task_arn, + container_name, + self._container.get_environment_variables, ) def get_container_secrets(self, cluster_name: str, task_arn: str, container_name: str) -> dict[str, str] | None: @@ -115,13 +135,19 @@ def get_container_secrets(self, cluster_name: str, task_arn: str, container_name return self._with_container_context(cluster_name, task_arn, container_name, self._container.get_secrets) def get_container_port_mappings( - self, cluster_name: str, task_arn: str, container_name: str + self, + cluster_name: str, + task_arn: str, + container_name: str, ) -> list[dict[str, Any]] | None: """Get port mappings for a specific container in a task.""" return self._with_container_context(cluster_name, task_arn, container_name, self._container.get_port_mappings) def get_container_volume_mounts( - self, cluster_name: str, task_arn: str, container_name: str + self, + cluster_name: str, + task_arn: str, + container_name: str, ) -> list[dict[str, Any]] | None: """Get volume mounts for a specific container in a task.""" return self._with_container_context(cluster_name, task_arn, container_name, self._container.get_volume_mounts) diff --git a/src/lazy_ecs/core/navigation.py b/src/lazy_ecs/core/navigation.py index 21a4aab..860e07d 100644 --- a/src/lazy_ecs/core/navigation.py +++ b/src/lazy_ecs/core/navigation.py @@ -50,7 +50,7 @@ def get_questionary_style() -> questionary.Style: ("pointer", "fg:cyan bold"), ("highlighted", "fg:cyan"), ("selected", "fg:green"), - ] + ], ) @@ -125,7 +125,10 @@ def _(event: KeyPressEvent) -> None: def select_with_pagination( - prompt: str, choices: list[dict[str, str]], back_text: str | None, page_size: int = 25 + prompt: str, + choices: list[dict[str, str]], + back_text: str | None, + page_size: int = 25, ) -> str | None: """Selection with pagination for large lists.""" total_items = len(choices) @@ -146,13 +149,14 @@ def select_with_pagination( if current_page < total_pages - 1: paginated_choices.append( questionary.Choice( - f"→ Next Page ({end_idx + 1}-{min(end_idx + page_size, total_items)})", "pagination:next" - ) + f"→ Next Page ({end_idx + 1}-{min(end_idx + page_size, total_items)})", + "pagination:next", + ), ) if current_page > 0: paginated_choices.append( - questionary.Choice(f"← Previous Page ({start_idx - page_size + 1}-{start_idx})", "pagination:previous") + questionary.Choice(f"← Previous Page ({start_idx - page_size + 1}-{start_idx})", "pagination:previous"), ) if back_text: @@ -161,7 +165,10 @@ def select_with_pagination( paginated_choices.append(questionary.Choice("āŒ Exit", "navigation:exit")) selected = questionary.select( - page_prompt, choices=paginated_choices, style=get_questionary_style(), use_shortcuts=False + page_prompt, + choices=paginated_choices, + style=get_questionary_style(), + use_shortcuts=False, ).ask() if selected == "pagination:next": @@ -173,7 +180,10 @@ def select_with_pagination( def select_with_auto_pagination( - prompt: str, choices: list[dict[str, str]], back_text: str | None, threshold: int = PAGINATION_THRESHOLD + prompt: str, + choices: list[dict[str, str]], + back_text: str | None, + threshold: int = PAGINATION_THRESHOLD, ) -> str | None: """Select with automatic pagination based on choice count. diff --git a/src/lazy_ecs/core/types.py b/src/lazy_ecs/core/types.py index 5ceab80..876b675 100644 --- a/src/lazy_ecs/core/types.py +++ b/src/lazy_ecs/core/types.py @@ -3,10 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, Any, TypedDict - -if TYPE_CHECKING: - pass +from typing import Any, TypedDict class ServiceInfo(TypedDict): diff --git a/src/lazy_ecs/core/utils.py b/src/lazy_ecs/core/utils.py index 85d5df6..d76f281 100644 --- a/src/lazy_ecs/core/utils.py +++ b/src/lazy_ecs/core/utils.py @@ -133,7 +133,7 @@ def wait_for_keypress(stop_event: threading.Event) -> str | None: char = sys.stdin.read(1) # Handle Ctrl-C properly if char == "\x03": # Ctrl-C - raise KeyboardInterrupt() + raise KeyboardInterrupt return char return None diff --git a/src/lazy_ecs/features/container/container.py b/src/lazy_ecs/features/container/container.py index 899e4ec..218356f 100644 --- a/src/lazy_ecs/features/container/container.py +++ b/src/lazy_ecs/features/container/container.py @@ -64,7 +64,9 @@ def get_container_context(self, cluster_name: str, task_arn: str, container_name return None def get_container_definition( - self, task_definition: TaskDefinitionTypeDef, container_name: str + self, + task_definition: TaskDefinitionTypeDef, + container_name: str, ) -> ContainerDefinitionOutputTypeDef | None: for container_def in task_definition["containerDefinitions"]: if container_def["name"] == container_name: @@ -104,12 +106,19 @@ def get_container_logs(self, log_group: str, log_stream: str, lines: int = 50) - if not self.logs_client: return [] response = self.logs_client.get_log_events( - logGroupName=log_group, logStreamName=log_stream, limit=lines, startFromHead=False + logGroupName=log_group, + logStreamName=log_stream, + limit=lines, + startFromHead=False, ) return response.get("events", []) def get_container_logs_filtered( - self, log_group: str, log_stream: str, filter_pattern: str, lines: int = 50 + self, + log_group: str, + log_stream: str, + filter_pattern: str, + lines: int = 50, ) -> list[FilteredLogEventTypeDef]: """Get container logs with CloudWatch filter pattern applied.""" if not self.logs_client: @@ -123,7 +132,10 @@ def get_container_logs_filtered( return response.get("events", []) def get_live_container_logs_tail( - self, log_group: str, log_stream: str, event_filter_pattern: str = "" + self, + log_group: str, + log_stream: str, + event_filter_pattern: str = "", ) -> Generator[StartLiveTailResponseStreamTypeDef | LiveTailSessionLogEventTypeDef]: """Get container logs in real time from CloudWatch.""" if not self.logs_client: diff --git a/src/lazy_ecs/features/container/ui.py b/src/lazy_ecs/features/container/ui.py index 652827f..769c6f0 100644 --- a/src/lazy_ecs/features/container/ui.py +++ b/src/lazy_ecs/features/container/ui.py @@ -54,7 +54,11 @@ def show_logs_live_tail(self, cluster_name: str, task_arn: str, container_name: filter_pattern = "" while True: action = self._display_logs_with_tail( - container_name, log_group_name, log_stream_name, filter_pattern, lines + container_name, + log_group_name, + log_stream_name, + filter_pattern, + lines, ) if action == Action.STOP: @@ -95,7 +99,10 @@ def _display_logs_with_tail( # Fetch and display recent logs if filter_pattern: events = self.container_service.get_container_logs_filtered( - log_group_name, log_stream_name, filter_pattern, lines + log_group_name, + log_stream_name, + filter_pattern, + lines, ) else: events = self.container_service.get_container_logs(log_group_name, log_stream_name, lines) @@ -139,12 +146,14 @@ def log_reader() -> None: log_generator = None try: log_generator = self.container_service.get_live_container_logs_tail( - log_group_name, log_stream_name, filter_pattern + log_group_name, + log_stream_name, + filter_pattern, ) for event in log_generator: if stop_event.is_set(): break - log_queue.put(cast(dict[str, Any], event)) + log_queue.put(cast("dict[str, Any]", event)) except Exception: pass # Iterator exhausted or error finally: diff --git a/src/lazy_ecs/features/service/ui.py b/src/lazy_ecs/features/service/ui.py index 39a33b3..8ffe6c7 100644 --- a/src/lazy_ecs/features/service/ui.py +++ b/src/lazy_ecs/features/service/ui.py @@ -50,13 +50,15 @@ def select_service_action(self, service_name: str, task_info: list[TaskInfo]) -> choices.append({"name": "šŸš€ Force new deployment", "value": "action:force_deployment"}) return select_with_auto_pagination( - f"Select action for service '{service_name}':", choices, "Back to cluster selection" + f"Select action for service '{service_name}':", + choices, + "Back to cluster selection", ) def handle_force_deployment(self, cluster_name: str, service_name: str) -> None: """Handle force deployment confirmation and execution.""" confirm = questionary.confirm( - f"Force new deployment for service '{service_name}' in cluster '{cluster_name}'?" + f"Force new deployment for service '{service_name}' in cluster '{cluster_name}'?", ).ask() if confirm: diff --git a/src/lazy_ecs/features/task/comparison.py b/src/lazy_ecs/features/task/comparison.py index e6e3625..765b2d4 100644 --- a/src/lazy_ecs/features/task/comparison.py +++ b/src/lazy_ecs/features/task/comparison.py @@ -72,7 +72,9 @@ def compare_task_definitions(source: dict[str, Any], target: dict[str, Any]) -> def _compare_task_level_resources( - source: dict[str, Any], target: dict[str, Any], changes: list[dict[str, Any]] + source: dict[str, Any], + target: dict[str, Any], + changes: list[dict[str, Any]], ) -> None: """Compare task-level CPU and memory.""" if source.get("taskCpu") != target.get("taskCpu"): @@ -81,7 +83,7 @@ def _compare_task_level_resources( "type": "task_cpu_changed", "old": source.get("taskCpu"), "new": target.get("taskCpu"), - } + }, ) if source.get("taskMemory") != target.get("taskMemory"): @@ -90,12 +92,14 @@ def _compare_task_level_resources( "type": "task_memory_changed", "old": source.get("taskMemory"), "new": target.get("taskMemory"), - } + }, ) def _compare_containers( - source_containers: list[dict[str, Any]], target_containers: list[dict[str, Any]], changes: list[dict[str, Any]] + source_containers: list[dict[str, Any]], + target_containers: list[dict[str, Any]], + changes: list[dict[str, Any]], ) -> None: """Compare containers between two task definitions.""" source_by_name = {c["name"]: c for c in source_containers} @@ -147,7 +151,7 @@ def _compare_dicts( for key, value in source.items(): if key not in target: changes.append( - {"type": f"{change_prefix}_removed", "container": container_name, "key": key, "value": value} + {"type": f"{change_prefix}_removed", "container": container_name, "key": key, "value": value}, ) elif target[key] != value: changes.append( @@ -157,7 +161,7 @@ def _compare_dicts( "key": key, "old": value, "new": target[key], - } + }, ) for key, value in target.items(): @@ -188,7 +192,9 @@ def list_task_definition_revisions(self, family: str, limit: int = 10) -> list[d return revisions[:limit] def get_task_definitions_for_comparison( - self, source_arn: str, target_arn: str + self, + source_arn: str, + target_arn: str, ) -> tuple[dict[str, Any], dict[str, Any]]: source_response = self.ecs_client.describe_task_definition(taskDefinition=source_arn) target_response = self.ecs_client.describe_task_definition(taskDefinition=target_arn) diff --git a/src/lazy_ecs/features/task/task.py b/src/lazy_ecs/features/task/task.py index 4dadd3a..4144a99 100644 --- a/src/lazy_ecs/features/task/task.py +++ b/src/lazy_ecs/features/task/task.py @@ -21,7 +21,11 @@ def __init__(self, ecs_client: ECSClient) -> None: def get_tasks(self, cluster_name: str, service_name: str) -> list[str]: return paginate_aws_list( - self.ecs_client, "list_tasks", "taskArns", cluster=cluster_name, serviceName=service_name + self.ecs_client, + "list_tasks", + "taskArns", + cluster=cluster_name, + serviceName=service_name, ) def get_task_info(self, cluster_name: str, service_name: str, desired_task_def_arn: str | None) -> list[TaskInfo]: @@ -38,7 +42,10 @@ def get_task_info(self, cluster_name: str, service_name: str, desired_task_def_a return [_create_task_info(task, desired_task_def_arn) for task in all_tasks] def get_task_details( - self, cluster_name: str, task_arn: str, desired_task_def_arn: str | None + self, + cluster_name: str, + task_arn: str, + desired_task_def_arn: str | None, ) -> TaskDetails | None: result = self.get_task_and_definition(cluster_name, task_arn) if not result: @@ -49,7 +56,9 @@ def get_task_details( return _build_task_details(task, task_definition, is_desired_version) def get_task_and_definition( - self, cluster_name: str, task_arn: str + self, + cluster_name: str, + task_arn: str, ) -> tuple[TaskTypeDef, TaskDefinitionTypeDef] | None: task_response = self.ecs_client.describe_tasks(cluster=cluster_name, tasks=[task_arn]) tasks = task_response.get("tasks", []) @@ -106,7 +115,11 @@ def get_task_failure_analysis(self, task_history: TaskHistoryDetails) -> str: if exit_code is not None and exit_code != 0: return self._analyze_container_failure( - container["name"], exit_code, container_reason, stop_code, stopped_reason + container["name"], + exit_code, + container_reason, + stop_code, + stopped_reason, ) # No container failures, analyze task-level issues @@ -129,7 +142,7 @@ def _parse_task_history(task: TaskTypeDef) -> TaskHistoryDetails: "reason": container.get("reason"), "health_status": container.get("healthStatus"), "last_status": container.get("lastStatus", "UNKNOWN"), - } + }, ) return { @@ -230,7 +243,9 @@ def _create_task_info(task: TaskTypeDef, desired_task_def_arn: str | None) -> Ta def _build_task_details( - task: TaskTypeDef, task_definition: TaskDefinitionTypeDef, is_desired_version: bool + task: TaskTypeDef, + task_definition: TaskDefinitionTypeDef, + is_desired_version: bool, ) -> TaskDetails: """Build comprehensive task details dictionary.""" task_arn = task["taskArn"] diff --git a/src/lazy_ecs/features/task/ui.py b/src/lazy_ecs/features/task/ui.py index 6b7de48..b94dd92 100644 --- a/src/lazy_ecs/features/task/ui.py +++ b/src/lazy_ecs/features/task/ui.py @@ -138,7 +138,7 @@ def select_task_feature(self, task_details: TaskDetails | None) -> str | None: {"name": "Show task history and failures", "value": "task_action:show_history"}, {"name": "Compare task definitions", "value": "task_action:compare_definitions"}, {"name": "🌐 Open in AWS console", "value": "task_action:open_console"}, - ] + ], ) for container in containers: @@ -165,7 +165,7 @@ def select_task_feature(self, task_details: TaskDetails | None) -> str | None: "name": f"Show volume mounts for '{container_name}'", "value": f"container_action:show_volumes:{container_name}", }, - ] + ], ) return select_with_auto_pagination("Select a feature for this task:", choices, "Back to service selection") @@ -183,7 +183,9 @@ def display_task_history(self, cluster_name: str, service_name: str) -> None: return sorted_history = sorted( - task_history, key=lambda t: t["created_at"] if t["created_at"] else datetime.min, reverse=True + task_history, + key=lambda t: t["created_at"] if t["created_at"] else datetime.min, + reverse=True, ) recent_tasks = sorted_history[:MAX_RECENT_TASKS] @@ -296,7 +298,9 @@ def show_task_definition_comparison(self, task_details: TaskDetails) -> None: ] selected_arn = select_with_auto_pagination( - f"Select revision to compare with v{current_revision}:", choices, "Back" + f"Select revision to compare with v{current_revision}:", + choices, + "Back", ) if not selected_arn: @@ -304,18 +308,23 @@ def show_task_definition_comparison(self, task_details: TaskDetails) -> None: with show_spinner(): source, target = self.comparison_service.get_task_definitions_for_comparison( - f"{family}:{current_revision}", selected_arn + f"{family}:{current_revision}", + selected_arn, ) changes = compare_task_definitions(source, target) self._display_comparison_results(source, target, changes) def _display_comparison_results( - self, source: dict[str, Any], target: dict[str, Any], changes: list[dict[str, Any]] + self, + source: dict[str, Any], + target: dict[str, Any], + changes: list[dict[str, Any]], ) -> None: """Display comparison results between two task definitions.""" console.print( - f"\nšŸ“Š Comparing: {source['family']}:v{source['revision']} → v{target['revision']}", style="bold cyan" + f"\nšŸ“Š Comparing: {source['family']}:v{source['revision']} → v{target['revision']}", + style="bold cyan", ) console.print("=" * 80, style="dim") diff --git a/tests/conftest.py b/tests/conftest.py index de3764f..5a9ae36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ def mock_paginated_client(): def test_something(mock_paginated_client): pages = [{"clusterArns": ["arn1", "arn2"]}] client = mock_paginated_client(pages) + """ def _create_client(pages: list[dict]) -> Mock: diff --git a/tests/test_aws_service.py b/tests/test_aws_service.py index f8e42f4..bf5b055 100644 --- a/tests/test_aws_service.py +++ b/tests/test_aws_service.py @@ -60,7 +60,7 @@ def ecs_client_with_tasks(): "logDriver": "awslogs", "options": {"awslogs-group": "/ecs/production/web", "awslogs-stream-prefix": "ecs"}, }, - } + }, ], ) @@ -80,7 +80,7 @@ def ecs_client_with_tasks(): "awsvpcConfiguration": { "subnets": ["subnet-12345"], "assignPublicIp": "ENABLED", - } + }, }, ) @@ -171,7 +171,7 @@ def ecs_client_with_secrets(): { "name": "SSL_CERT", "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:ssl-cert-MnOpQr", - } + }, ], }, ], @@ -244,7 +244,10 @@ def test_get_services_pagination(): for i in range(200): client.create_service( - cluster="production", serviceName=f"service-{i:03d}", taskDefinition="app-task", desiredCount=1 + cluster="production", + serviceName=f"service-{i:03d}", + taskDefinition="app-task", + desiredCount=1, ) service = ECSService(client) @@ -423,7 +426,7 @@ def test_get_log_config_no_config(): "awsvpcConfiguration": { "subnets": ["subnet-12345"], "assignPublicIp": "ENABLED", - } + }, }, ) @@ -703,7 +706,7 @@ def test_get_container_port_mappings_success() -> None: {"containerPort": 80, "hostPort": 8080, "protocol": "tcp"}, {"containerPort": 443, "hostPort": 0, "protocol": "tcp"}, ], - } + }, ], ) diff --git a/tests/test_container_ui.py b/tests/test_container_ui.py index c8af413..d27f305 100644 --- a/tests/test_container_ui.py +++ b/tests/test_container_ui.py @@ -85,7 +85,7 @@ def test_show_logs_live_tail_with_filter_exclude(container_ui): container_ui.container_service.get_container_logs_filtered = Mock(return_value=filtered_events) # Return different iterators for each call container_ui.container_service.get_live_container_logs_tail = Mock( - side_effect=[iter(live_events_first), iter(live_events_second)] + side_effect=[iter(live_events_first), iter(live_events_second)], ) # Mock the queues to simulate pressing 'f' for filter then 's' to stop @@ -115,7 +115,10 @@ def test_show_logs_live_tail_with_filter_exclude(container_ui): # Should be called with -healthcheck filter pattern container_ui.container_service.get_container_logs_filtered.assert_called_once_with( - "test-log-group", "test-stream", "-healthcheck", 50 + "test-log-group", + "test-stream", + "-healthcheck", + 50, ) @@ -136,7 +139,7 @@ def test_get_container_logs_filtered(mock_ecs_client, mock_task_service): mock_logs_client.filter_log_events.return_value = { "events": [ {"timestamp": 1234567890000, "message": "ERROR: Something failed"}, - ] + ], } container_service = ContainerService(mock_ecs_client, mock_task_service, None, mock_logs_client) @@ -163,7 +166,9 @@ def test_show_container_environment_variables_success(container_ui): container_ui.show_container_environment_variables("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_environment_variables.assert_called_once_with(context) @@ -175,7 +180,9 @@ def test_show_container_environment_variables_no_context(container_ui): container_ui.show_container_environment_variables("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) @@ -189,7 +196,9 @@ def test_show_container_environment_variables_empty(container_ui): container_ui.show_container_environment_variables("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_environment_variables.assert_called_once_with(context) @@ -208,7 +217,9 @@ def test_show_container_secrets_success(container_ui): container_ui.show_container_secrets("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_secrets.assert_called_once_with(context) @@ -220,7 +231,9 @@ def test_show_container_secrets_no_context(container_ui): container_ui.show_container_secrets("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) @@ -234,7 +247,9 @@ def test_show_container_secrets_empty(container_ui): container_ui.show_container_secrets("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_secrets.assert_called_once_with(context) @@ -250,7 +265,9 @@ def test_show_container_port_mappings_success(container_ui): container_ui.show_container_port_mappings("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_port_mappings.assert_called_once_with(context) @@ -262,7 +279,9 @@ def test_show_container_port_mappings_no_context(container_ui): container_ui.show_container_port_mappings("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) @@ -276,7 +295,9 @@ def test_show_container_port_mappings_empty(container_ui): container_ui.show_container_port_mappings("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_port_mappings.assert_called_once_with(context) @@ -285,7 +306,7 @@ def test_show_container_volume_mounts_success(container_ui): """Test displaying container volume mounts successfully.""" context = {"container_definition": {"mountPoints": []}} volume_mounts = [ - {"source_volume": "data-vol", "container_path": "/data", "read_only": False, "host_path": "/host/data"} + {"source_volume": "data-vol", "container_path": "/data", "read_only": False, "host_path": "/host/data"}, ] container_ui.container_service.get_container_context = Mock(return_value=context) @@ -294,7 +315,9 @@ def test_show_container_volume_mounts_success(container_ui): container_ui.show_container_volume_mounts("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_volume_mounts.assert_called_once_with(context) @@ -306,7 +329,9 @@ def test_show_container_volume_mounts_no_context(container_ui): container_ui.show_container_volume_mounts("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) @@ -320,6 +345,8 @@ def test_show_container_volume_mounts_empty(container_ui): container_ui.show_container_volume_mounts("test-cluster", "task-arn", "web-container") container_ui.container_service.get_container_context.assert_called_once_with( - "test-cluster", "task-arn", "web-container" + "test-cluster", + "task-arn", + "web-container", ) container_ui.container_service.get_volume_mounts.assert_called_once_with(context) diff --git a/tests/test_service_events.py b/tests/test_service_events.py index b37bc3c..6ad090d 100644 --- a/tests/test_service_events.py +++ b/tests/test_service_events.py @@ -132,9 +132,9 @@ def test_service_events_sorted_by_time(): "createdAt": datetime(2024, 1, 15, 11, 0, 0), "message": "Middle event", }, - ] - } - ] + ], + }, + ], } service = ServiceService(mock_client) diff --git a/tests/test_service_metrics.py b/tests/test_service_metrics.py index d3fe4a4..c759587 100644 --- a/tests/test_service_metrics.py +++ b/tests/test_service_metrics.py @@ -36,7 +36,7 @@ def cloudwatch_client_with_metrics(): {"Name": "ClusterName", "Value": cluster_name}, {"Name": "ServiceName", "Value": service_name}, ], - } + }, ], ) @@ -52,7 +52,7 @@ def cloudwatch_client_with_metrics(): {"Name": "ClusterName", "Value": cluster_name}, {"Name": "ServiceName", "Value": service_name}, ], - } + }, ], ) diff --git a/tests/test_service_ui.py b/tests/test_service_ui.py index 11c4aa5..91602d4 100644 --- a/tests/test_service_ui.py +++ b/tests/test_service_ui.py @@ -34,8 +34,8 @@ def test_select_service_with_services(mock_select, service_ui): "running_count": 2, "desired_count": 2, "pending_count": 0, - } - ] + }, + ], ) mock_select.return_value = "service:web-api" @@ -56,7 +56,7 @@ def test_select_service_with_many_services(mock_select, service_ui): "running_count": 1, "desired_count": 1, "pending_count": 0, - } + }, ) service_ui.service_service.get_service_info = Mock(return_value=service_info) mock_select.return_value = "service:service-50" @@ -91,8 +91,8 @@ def test_select_service_navigation_back(mock_select, service_ui): "running_count": 2, "desired_count": 2, "pending_count": 0, - } - ] + }, + ], ) mock_select.return_value.ask.return_value = "navigation:back" @@ -216,7 +216,7 @@ def test_service_name_truncation_shows_end(mock_print, service_ui): "created_at": datetime(2024, 1, 15, 10, 30, 45), "message": "(service very-long-service-name-with-important-suffix-v2) has started a deployment", "event_type": "deployment", - } + }, ] service_ui.service_service.get_service_events = Mock(return_value=mock_events) diff --git a/tests/test_task_comparison.py b/tests/test_task_comparison.py index 6bb052a..c531905 100644 --- a/tests/test_task_comparison.py +++ b/tests/test_task_comparison.py @@ -21,7 +21,7 @@ def test_normalize_task_definition_strips_aws_metadata(): "image": "nginx:1.21", "cpu": 256, "memory": 512, - } + }, ], } @@ -61,7 +61,7 @@ def test_normalize_task_definition_extracts_container_fields(): }, "command": ["npm", "start"], "entryPoint": ["/bin/sh"], - } + }, ], } @@ -88,7 +88,7 @@ def test_normalize_task_definition_handles_missing_fields(): { "name": "web", "image": "nginx:1.21", - } + }, ], } @@ -149,7 +149,7 @@ def test_compare_task_definitions_detects_env_var_changes(): "name": "web", "image": "nginx:1.21", "environment": {"ENV": "staging", "DEBUG": "false"}, - } + }, ], } target = { @@ -160,7 +160,7 @@ def test_compare_task_definitions_detects_env_var_changes(): "name": "web", "image": "nginx:1.21", "environment": {"ENV": "production", "LOG_LEVEL": "info"}, - } + }, ], } @@ -219,7 +219,7 @@ def test_compare_task_definitions_detects_secret_changes(): "name": "web", "image": "nginx:1.21", "secrets": {"API_KEY": "arn:aws:secretsmanager:us-east-1:123:secret:api-key-v1"}, - } + }, ], } target = { @@ -230,7 +230,7 @@ def test_compare_task_definitions_detects_secret_changes(): "name": "web", "image": "nginx:1.21", "secrets": {"API_KEY": "arn:aws:secretsmanager:us-east-1:123:secret:api-key-v2"}, - } + }, ], } @@ -267,7 +267,7 @@ def test_compare_task_definitions_detects_port_changes(): "name": "web", "image": "nginx:1.21", "ports": [{"containerPort": 80, "protocol": "tcp"}], - } + }, ], } target = { @@ -278,7 +278,7 @@ def test_compare_task_definitions_detects_port_changes(): "name": "web", "image": "nginx:1.21", "ports": [{"containerPort": 8080, "protocol": "tcp"}], - } + }, ], } @@ -340,7 +340,7 @@ def test_compare_task_definitions_detects_volume_changes(): "name": "web", "image": "nginx:1.21", "mountPoints": [{"sourceVolume": "data", "containerPath": "/data"}], - } + }, ], } target = { @@ -351,7 +351,7 @@ def test_compare_task_definitions_detects_volume_changes(): "name": "web", "image": "nginx:1.21", "mountPoints": [{"sourceVolume": "data", "containerPath": "/var/data"}], - } + }, ], } diff --git a/tests/test_task_comparison_service.py b/tests/test_task_comparison_service.py index 4a7bc39..142a67e 100644 --- a/tests/test_task_comparison_service.py +++ b/tests/test_task_comparison_service.py @@ -22,7 +22,7 @@ def ecs_client_with_task_definitions(): "image": "nginx:1.19", "memory": 512, "environment": [{"name": "ENV", "value": "dev"}], - } + }, ], ) @@ -34,7 +34,7 @@ def ecs_client_with_task_definitions(): "image": "nginx:1.20", "memory": 512, "environment": [{"name": "ENV", "value": "staging"}], - } + }, ], ) @@ -46,7 +46,7 @@ def ecs_client_with_task_definitions(): "image": "nginx:1.21", "memory": 1024, "environment": [{"name": "ENV", "value": "production"}], - } + }, ], ) diff --git a/tests/test_task_history.py b/tests/test_task_history.py index 1abbd59..26c63f1 100644 --- a/tests/test_task_history.py +++ b/tests/test_task_history.py @@ -32,7 +32,7 @@ def test_parse_stopped_task_with_stop_code(self): "reason": "OutOfMemoryError: Container killed due to memory usage", "healthStatus": "UNHEALTHY", "lastStatus": "STOPPED", - } + }, ], } @@ -54,7 +54,7 @@ def test_parse_stopped_task_with_stop_code(self): "reason": "OutOfMemoryError: Container killed due to memory usage", "health_status": "UNHEALTHY", "last_status": "STOPPED", - } + }, ], } @@ -91,7 +91,7 @@ def test_parse_running_task_no_failure_info(self): "reason": None, "health_status": "HEALTHY", "last_status": "RUNNING", - } + }, ], } @@ -120,7 +120,11 @@ def test_analyze_successful_completion(self): def test_analyze_timeout_failure(self): """Test analysis of timeout failure.""" result = TaskService._analyze_container_failure( - "web-api", 137, "Task killed", "TaskFailedToStart", "Task timed out" + "web-api", + 137, + "Task killed", + "TaskFailedToStart", + "Task timed out", ) assert "ā°" in result assert "timeout" in result.lower() @@ -137,8 +141,8 @@ def mock_ecs_client(self, mock_paginated_client): "taskArns": [ "arn:aws:ecs:us-east-1:123456789012:task/cluster/running-task", "arn:aws:ecs:us-east-1:123456789012:task/cluster/stopped-task", - ] - } + ], + }, ] client = mock_paginated_client(pages) @@ -151,8 +155,8 @@ def mock_ecs_client(self, mock_paginated_client): "stoppedReason": "Essential container in task exited", "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/web-api:5", "containers": [{"name": "web-api", "exitCode": 137}], - } - ] + }, + ], } return client diff --git a/tests/test_task_history_ui.py b/tests/test_task_history_ui.py index a7e682d..61782be 100644 --- a/tests/test_task_history_ui.py +++ b/tests/test_task_history_ui.py @@ -44,7 +44,7 @@ def sample_task_history(self): "reason": None, "health_status": "HEALTHY", "last_status": "RUNNING", - } + }, ], }, { @@ -65,7 +65,7 @@ def sample_task_history(self): "reason": "OutOfMemoryError: Container killed due to memory usage", "health_status": "UNHEALTHY", "last_status": "STOPPED", - } + }, ], }, ] @@ -125,7 +125,7 @@ def test_display_failure_analysis(self, mock_print, task_ui, mock_task_service): "reason": "OutOfMemoryError: Container killed due to memory usage", "health_status": "UNHEALTHY", "last_status": "STOPPED", - } + }, ], } diff --git a/tests/test_task_ui.py b/tests/test_task_ui.py index 5f06dec..85a1424 100644 --- a/tests/test_task_ui.py +++ b/tests/test_task_ui.py @@ -62,7 +62,7 @@ def test_display_task_details_success(task_ui): "is_desired_version": True, "task_status": "RUNNING", "containers": [ - {"name": "web-api", "image": "nginx:latest", "cpu": 256, "memory": 512, "memoryReservation": None} + {"name": "web-api", "image": "nginx:latest", "cpu": 256, "memory": 512, "memoryReservation": None}, ], "created_at": None, "started_at": None, diff --git a/tests/test_ui.py b/tests/test_ui.py index ec13219..aaed3eb 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -57,7 +57,8 @@ def test_select_service_action_integration(mock_ecs_service) -> None: assert result == "task:show_details:task-arn-1" mock_ecs_service.get_task_info.assert_called_once_with("production", "web-api") navigator._service_ui.select_service_action.assert_called_once_with( - "web-api", [{"name": "task-1", "value": "task-arn-1"}] + "web-api", + [{"name": "task-1", "value": "task-arn-1"}], ) From de9937134a0bbda604483a24e9be617941516cef Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 6 Oct 2025 10:44:54 +0300 Subject: [PATCH 8/8] bump version to 0.7.2 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02419e9..d6d8194 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lazy-ecs" -version = "0.7.1" +version = "0.7.2" description = "A CLI tool for working with AWS services" readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index 215d77b..e30da44 100644 --- a/uv.lock +++ b/uv.lock @@ -384,7 +384,7 @@ wheels = [ [[package]] name = "lazy-ecs" -version = "0.7.1" +version = "0.7.2" source = { editable = "." } dependencies = [ { name = "boto3" },