qodalis-cli-server-plugin-aws 1.0.0b13__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ """Qodalis CLI AWS plugin — AWS cloud resource management commands."""
2
+
3
+ from .aws_module import AwsModule
4
+
5
+ __all__ = ["AwsModule"]
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Sequence
4
+
5
+ from qodalis_cli_server_abstractions import (
6
+ CliModule,
7
+ ICliCommandProcessor,
8
+ )
9
+
10
+ from .processors.aws_command_processor import AwsCommandProcessor
11
+
12
+
13
+ class AwsModule(CliModule):
14
+ """AWS cloud resource management module."""
15
+
16
+ @property
17
+ def name(self) -> str:
18
+ """Returns the module name."""
19
+ return "aws"
20
+
21
+ @property
22
+ def version(self) -> str:
23
+ """Returns the module version."""
24
+ return "1.0.0"
25
+
26
+ @property
27
+ def description(self) -> str:
28
+ """Returns a human-readable description of the module."""
29
+ return "Provides AWS cloud resource management commands"
30
+
31
+ @property
32
+ def processors(self) -> Sequence[ICliCommandProcessor]:
33
+ """Returns the command processors provided by this module."""
34
+ return [AwsCommandProcessor()]
File without changes
@@ -0,0 +1,297 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from qodalis_cli_server_abstractions import (
11
+ CliCommandParameterDescriptor,
12
+ CliCommandProcessor,
13
+ CliProcessCommand,
14
+ ICliCommandParameterDescriptor,
15
+ ICliCommandProcessor,
16
+ )
17
+
18
+ from ..services.aws_config_service import AwsConfigService
19
+ from ..services.aws_credential_manager import AwsCredentialManager
20
+ from ..utils.output_helpers import (
21
+ build_response,
22
+ build_error_response,
23
+ build_success_response,
24
+ format_as_key_value,
25
+ format_as_list,
26
+ CliServerTextOutput,
27
+ )
28
+ from .s3_processor import AwsS3Processor
29
+ from .ec2_processor import AwsEc2Processor
30
+ from .lambda_processor import AwsLambdaProcessor
31
+ from .cloudwatch_processor import AwsCloudWatchProcessor
32
+ from .sns_processor import AwsSnsProcessor
33
+ from .sqs_processor import AwsSqsProcessor
34
+ from .iam_processor import AwsIamProcessor
35
+ from .dynamodb_processor import AwsDynamoDbProcessor
36
+ from .ecs_processor import AwsEcsProcessor
37
+
38
+
39
+ class _AwsConfigureSetProcessor(CliCommandProcessor):
40
+ """Handles ``aws configure set`` to update AWS credentials and region."""
41
+
42
+ def __init__(self, config_service: AwsConfigService, credential_manager: AwsCredentialManager) -> None:
43
+ super().__init__()
44
+ self._config_service = config_service
45
+ self._credential_manager = credential_manager
46
+
47
+ @property
48
+ def command(self) -> str:
49
+ return "set"
50
+
51
+ @property
52
+ def description(self) -> str:
53
+ return "Set AWS credentials and region"
54
+
55
+ @property
56
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
57
+ return [
58
+ CliCommandParameterDescriptor(name="key", description="AWS access key ID", required=False, type="string", aliases=["-k"]),
59
+ CliCommandParameterDescriptor(name="secret", description="AWS secret access key", required=False, type="string", aliases=["-s"]),
60
+ CliCommandParameterDescriptor(name="region", description="AWS region", required=False, type="string", aliases=["-r"]),
61
+ CliCommandParameterDescriptor(name="profile", description="AWS profile name", required=False, type="string", aliases=["-p"], default_value="default"),
62
+ ]
63
+
64
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
65
+ return ""
66
+
67
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
68
+ """Applies credential, region, and profile updates from arguments."""
69
+ key = command.args.get("key")
70
+ secret = command.args.get("secret")
71
+ region = command.args.get("region")
72
+ profile = command.args.get("profile")
73
+
74
+ if key and secret:
75
+ self._config_service.set_credentials(str(key), str(secret))
76
+ self._credential_manager.clear_cache()
77
+ elif key or secret:
78
+ return build_error_response("Both --key and --secret must be provided together.")
79
+
80
+ if region:
81
+ self._config_service.set_region(str(region))
82
+ self._credential_manager.clear_cache()
83
+
84
+ if profile:
85
+ self._config_service.set_profile(str(profile))
86
+ self._credential_manager.clear_cache()
87
+
88
+ if not key and not secret and not region and not profile:
89
+ return build_error_response("Provide at least one of --key/--secret, --region, or --profile.")
90
+
91
+ return build_success_response("AWS configuration updated.")
92
+
93
+
94
+ class _AwsConfigureGetProcessor(CliCommandProcessor):
95
+ """Handles ``aws configure get`` to display current configuration."""
96
+
97
+ def __init__(self, config_service: AwsConfigService) -> None:
98
+ super().__init__()
99
+ self._config_service = config_service
100
+
101
+ @property
102
+ def command(self) -> str:
103
+ return "get"
104
+
105
+ @property
106
+ def description(self) -> str:
107
+ return "Show current AWS configuration (secrets masked)"
108
+
109
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
110
+ return ""
111
+
112
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
113
+ """Returns the current AWS config summary with secrets masked."""
114
+ summary = self._config_service.get_config_summary()
115
+ entries = {
116
+ "Access Key ID": summary.get("access_key_id") or "(not set)",
117
+ "Secret Access Key": summary.get("secret_access_key") or "(not set)",
118
+ "Region": summary.get("region") or "(not set)",
119
+ "Profile": summary.get("profile") or "default",
120
+ }
121
+ return build_response([format_as_key_value(entries)])
122
+
123
+
124
+ class _AwsConfigureProfilesProcessor(CliCommandProcessor):
125
+ """Handles ``aws configure profiles`` to list locally available AWS profiles."""
126
+
127
+ @property
128
+ def command(self) -> str:
129
+ return "profiles"
130
+
131
+ @property
132
+ def description(self) -> str:
133
+ return "List available AWS profiles from ~/.aws/credentials and ~/.aws/config"
134
+
135
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
136
+ return ""
137
+
138
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
139
+ """Parses AWS config files and returns discovered profile names."""
140
+ profiles: set[str] = set()
141
+
142
+ home = Path.home()
143
+ credentials_path = home / ".aws" / "credentials"
144
+ config_path = home / ".aws" / "config"
145
+
146
+ self._parse_profiles(credentials_path, re.compile(r"^\[([^\]]+)\]"), profiles)
147
+ self._parse_profiles(config_path, re.compile(r"^\[(?:profile\s+)?([^\]]+)\]"), profiles)
148
+
149
+ items = sorted(profiles)
150
+
151
+ if not items:
152
+ return build_response([
153
+ CliServerTextOutput(value="No AWS profiles found in ~/.aws/credentials or ~/.aws/config.", style="warning"),
154
+ ])
155
+
156
+ return build_response([format_as_list(items)])
157
+
158
+ @staticmethod
159
+ def _parse_profiles(file_path: Path, pattern: re.Pattern[str], profiles: set[str]) -> None:
160
+ """Extracts profile names from an AWS config or credentials file.
161
+
162
+ Args:
163
+ file_path: Path to the AWS config file.
164
+ pattern: Regex to match profile section headers.
165
+ profiles: Set to add discovered profile names to.
166
+ """
167
+ try:
168
+ content = file_path.read_text(encoding="utf-8")
169
+ for line in content.splitlines():
170
+ match = pattern.match(line.strip())
171
+ if match:
172
+ profiles.add(match.group(1).strip())
173
+ except (OSError, IOError):
174
+ pass
175
+
176
+
177
+ class _AwsConfigureProcessor(CliCommandProcessor):
178
+ """Parent processor for ``aws configure`` sub-commands."""
179
+
180
+ def __init__(self, config_service: AwsConfigService, credential_manager: AwsCredentialManager) -> None:
181
+ super().__init__()
182
+ self._sub_processors: list[ICliCommandProcessor] = [
183
+ _AwsConfigureSetProcessor(config_service, credential_manager),
184
+ _AwsConfigureGetProcessor(config_service),
185
+ _AwsConfigureProfilesProcessor(),
186
+ ]
187
+
188
+ @property
189
+ def command(self) -> str:
190
+ return "configure"
191
+
192
+ @property
193
+ def description(self) -> str:
194
+ return "Manage AWS credentials and configuration"
195
+
196
+ @property
197
+ def processors(self) -> list[ICliCommandProcessor]:
198
+ """Returns sub-processors for set, get, and profiles commands."""
199
+ return self._sub_processors
200
+
201
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
202
+ return ""
203
+
204
+
205
+ class _AwsStatusProcessor(CliCommandProcessor):
206
+ """Handles ``aws status`` to verify AWS connectivity via STS."""
207
+
208
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
209
+ super().__init__()
210
+ self._credential_manager = credential_manager
211
+
212
+ @property
213
+ def command(self) -> str:
214
+ return "status"
215
+
216
+ @property
217
+ def description(self) -> str:
218
+ return "Test AWS connectivity using STS GetCallerIdentity"
219
+
220
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
221
+ return ""
222
+
223
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
224
+ """Calls STS GetCallerIdentity and returns account details."""
225
+ try:
226
+ client = self._credential_manager.get_client("sts")
227
+ response = client.get_caller_identity()
228
+
229
+ entries = {
230
+ "Account": response.get("Account", "(unknown)"),
231
+ "Arn": response.get("Arn", "(unknown)"),
232
+ "UserId": response.get("UserId", "(unknown)"),
233
+ }
234
+
235
+ return build_response([
236
+ CliServerTextOutput(value="AWS connection successful.", style="success"),
237
+ format_as_key_value(entries),
238
+ ])
239
+ except Exception as exc:
240
+ err_name = type(exc).__name__
241
+ err_msg = str(exc)
242
+ if "credentials" in err_msg.lower() or err_name in ("NoCredentialsError", "PartialCredentialsError"):
243
+ return build_error_response(
244
+ 'No AWS credentials configured. Run "aws configure set --key <key> --secret <secret>" '
245
+ "or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables."
246
+ )
247
+ return build_error_response(f"AWS status check failed: {exc}")
248
+
249
+
250
+ class AwsCommandProcessor(CliCommandProcessor):
251
+ """Root processor for all ``aws`` commands.
252
+
253
+ Registers sub-processors for configure, status, and all AWS service
254
+ commands (S3, EC2, Lambda, CloudWatch, SNS, SQS, ECS, DynamoDB, IAM).
255
+ """
256
+
257
+ def __init__(self) -> None:
258
+ super().__init__()
259
+ self._config_service = AwsConfigService()
260
+ self._credential_manager = AwsCredentialManager(self._config_service)
261
+ self._sub_processors: list[ICliCommandProcessor] = [
262
+ _AwsConfigureProcessor(self._config_service, self._credential_manager),
263
+ _AwsStatusProcessor(self._credential_manager),
264
+ AwsS3Processor(self._credential_manager),
265
+ AwsEc2Processor(self._credential_manager),
266
+ AwsLambdaProcessor(self._credential_manager),
267
+ AwsCloudWatchProcessor(self._credential_manager),
268
+ AwsSnsProcessor(self._credential_manager),
269
+ AwsSqsProcessor(self._credential_manager),
270
+ AwsEcsProcessor(self._credential_manager),
271
+ AwsDynamoDbProcessor(self._credential_manager),
272
+ AwsIamProcessor(self._credential_manager),
273
+ ]
274
+
275
+ @property
276
+ def command(self) -> str:
277
+ return "aws"
278
+
279
+ @property
280
+ def description(self) -> str:
281
+ return "AWS cloud resource management"
282
+
283
+ @property
284
+ def processors(self) -> list[ICliCommandProcessor]:
285
+ """Returns all registered AWS service sub-processors."""
286
+ return self._sub_processors
287
+
288
+ def get_credential_manager(self) -> AwsCredentialManager:
289
+ """Returns the shared credential manager instance."""
290
+ return self._credential_manager
291
+
292
+ def get_config_service(self) -> AwsConfigService:
293
+ """Returns the shared config service instance."""
294
+ return self._config_service
295
+
296
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
297
+ return ""
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from typing import Any
6
+
7
+ from qodalis_cli_server_abstractions import (
8
+ get_positional_text,
9
+ CliCommandParameterDescriptor,
10
+ CliCommandProcessor,
11
+ CliProcessCommand,
12
+ ICliCommandParameterDescriptor,
13
+ ICliCommandProcessor,
14
+ )
15
+
16
+ from ..services.aws_credential_manager import AwsCredentialManager
17
+ from ..utils.output_helpers import (
18
+ build_response,
19
+ build_error_response,
20
+ format_as_table,
21
+ apply_output_format,
22
+ CliServerTextOutput,
23
+ )
24
+
25
+
26
+ class _CloudWatchAlarmsProcessor(CliCommandProcessor):
27
+ """Lists CloudWatch metric alarms."""
28
+
29
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
30
+ super().__init__()
31
+ self._credential_manager = credential_manager
32
+
33
+ @property
34
+ def command(self) -> str:
35
+ return "alarms"
36
+
37
+ @property
38
+ def description(self) -> str:
39
+ return "List CloudWatch alarms"
40
+
41
+ @property
42
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
43
+ return [
44
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
45
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
46
+ ]
47
+
48
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
49
+ return ""
50
+
51
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
52
+ """Fetches and returns all CloudWatch metric alarms."""
53
+ region = command.args.get("region")
54
+ client = self._credential_manager.get_client("cloudwatch", region=str(region) if region else None)
55
+
56
+ try:
57
+ response = client.describe_alarms()
58
+ alarms = response.get("MetricAlarms", [])
59
+
60
+ if not alarms:
61
+ return build_response([CliServerTextOutput(value="No alarms found.", style="warning")])
62
+
63
+ rows = [
64
+ [
65
+ a.get("AlarmName", "(unknown)"),
66
+ a.get("StateValue", "(unknown)"),
67
+ a.get("MetricName", "(unknown)"),
68
+ a.get("Namespace", "(unknown)"),
69
+ ]
70
+ for a in alarms
71
+ ]
72
+
73
+ table_output = format_as_table(["Name", "State", "Metric", "Namespace"], rows)
74
+ return build_response([apply_output_format(command, table_output, alarms)])
75
+ except Exception as exc:
76
+ return build_error_response(f"Failed to list alarms: {exc}")
77
+
78
+
79
+ class _CloudWatchLogsProcessor(CliCommandProcessor):
80
+ """Fetches log events from a CloudWatch log group."""
81
+
82
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
83
+ super().__init__()
84
+ self._credential_manager = credential_manager
85
+
86
+ @property
87
+ def command(self) -> str:
88
+ return "logs"
89
+
90
+ @property
91
+ def description(self) -> str:
92
+ return "Fetch log events from a CloudWatch log group"
93
+
94
+ @property
95
+ def value_required(self) -> bool | None:
96
+ return True
97
+
98
+ @property
99
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
100
+ return [
101
+ CliCommandParameterDescriptor(name="filter", description="Filter pattern for log events", required=False, type="string", aliases=["-f"]),
102
+ CliCommandParameterDescriptor(name="limit", description="Maximum number of events to return (default: 50)", required=False, type="number", aliases=["-l"], default_value="50"),
103
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
104
+ ]
105
+
106
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
107
+ return ""
108
+
109
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
110
+ """Retrieves and formats recent log events from the specified log group."""
111
+ log_group_name = get_positional_text(command)
112
+ if not log_group_name:
113
+ return build_error_response("Log group name is required. Usage: cloudwatch logs <log-group>")
114
+
115
+ region = command.args.get("region")
116
+ client = self._credential_manager.get_client("logs", region=str(region) if region else None)
117
+
118
+ try:
119
+ limit = int(command.args.get("limit", 50))
120
+ except (ValueError, TypeError):
121
+ limit = 50
122
+
123
+ filter_pattern = command.args.get("filter")
124
+
125
+ try:
126
+ kwargs: dict[str, Any] = {"logGroupName": log_group_name, "limit": limit}
127
+ if filter_pattern:
128
+ kwargs["filterPattern"] = str(filter_pattern)
129
+
130
+ response = client.filter_log_events(**kwargs)
131
+ events = response.get("events", [])
132
+
133
+ if not events:
134
+ return build_response([CliServerTextOutput(value="No log events found.", style="warning")])
135
+
136
+ from datetime import datetime, timezone
137
+
138
+ lines = []
139
+ for e in events:
140
+ ts = e.get("timestamp")
141
+ if ts:
142
+ timestamp = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat()
143
+ else:
144
+ timestamp = "(unknown)"
145
+ message = (e.get("message") or "").rstrip()
146
+ lines.append(f"{timestamp} {message}")
147
+
148
+ return build_response([CliServerTextOutput(value="\n".join(lines))])
149
+ except Exception as exc:
150
+ return build_error_response(f"Failed to fetch log events: {exc}")
151
+
152
+
153
+ class _CloudWatchMetricsProcessor(CliCommandProcessor):
154
+ """Lists CloudWatch metrics for a given namespace."""
155
+
156
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
157
+ super().__init__()
158
+ self._credential_manager = credential_manager
159
+
160
+ @property
161
+ def command(self) -> str:
162
+ return "metrics"
163
+
164
+ @property
165
+ def description(self) -> str:
166
+ return "List CloudWatch metrics for a namespace"
167
+
168
+ @property
169
+ def value_required(self) -> bool | None:
170
+ return True
171
+
172
+ @property
173
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
174
+ return [
175
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
176
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
177
+ ]
178
+
179
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
180
+ return ""
181
+
182
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
183
+ """Queries metrics for the specified CloudWatch namespace."""
184
+ namespace = get_positional_text(command)
185
+ if not namespace:
186
+ return build_error_response("Namespace is required. Usage: cloudwatch metrics <namespace>")
187
+
188
+ region = command.args.get("region")
189
+ client = self._credential_manager.get_client("cloudwatch", region=str(region) if region else None)
190
+
191
+ try:
192
+ response = client.list_metrics(Namespace=namespace)
193
+ metrics = response.get("Metrics", [])
194
+
195
+ if not metrics:
196
+ return build_response([CliServerTextOutput(value=f'No metrics found for namespace "{namespace}".', style="warning")])
197
+
198
+ rows = [
199
+ [
200
+ m.get("MetricName", "(unknown)"),
201
+ ", ".join(f"{d.get('Name', '')}={d.get('Value', '')}" for d in m.get("Dimensions", [])),
202
+ ]
203
+ for m in metrics
204
+ ]
205
+
206
+ table_output = format_as_table(["MetricName", "Dimensions"], rows)
207
+ return build_response([apply_output_format(command, table_output, metrics)])
208
+ except Exception as exc:
209
+ return build_error_response(f"Failed to list metrics: {exc}")
210
+
211
+
212
+ class AwsCloudWatchProcessor(CliCommandProcessor):
213
+ """Parent processor for CloudWatch sub-commands (alarms, logs, metrics)."""
214
+
215
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
216
+ super().__init__()
217
+ self._sub_processors: list[ICliCommandProcessor] = [
218
+ _CloudWatchAlarmsProcessor(credential_manager),
219
+ _CloudWatchLogsProcessor(credential_manager),
220
+ _CloudWatchMetricsProcessor(credential_manager),
221
+ ]
222
+
223
+ @property
224
+ def command(self) -> str:
225
+ return "cloudwatch"
226
+
227
+ @property
228
+ def description(self) -> str:
229
+ return "Amazon CloudWatch -- alarms, logs, and metrics"
230
+
231
+ @property
232
+ def processors(self) -> list[ICliCommandProcessor]:
233
+ """Returns sub-processors for alarms, logs, and metrics."""
234
+ return self._sub_processors
235
+
236
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
237
+ return ""