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.
- qodalis_cli_aws/__init__.py +5 -0
- qodalis_cli_aws/aws_module.py +34 -0
- qodalis_cli_aws/processors/__init__.py +0 -0
- qodalis_cli_aws/processors/aws_command_processor.py +297 -0
- qodalis_cli_aws/processors/cloudwatch_processor.py +237 -0
- qodalis_cli_aws/processors/dynamodb_processor.py +274 -0
- qodalis_cli_aws/processors/ec2_processor.py +405 -0
- qodalis_cli_aws/processors/ecs_processor.py +236 -0
- qodalis_cli_aws/processors/iam_processor.py +208 -0
- qodalis_cli_aws/processors/lambda_processor.py +247 -0
- qodalis_cli_aws/processors/s3_processor.py +446 -0
- qodalis_cli_aws/processors/sns_processor.py +205 -0
- qodalis_cli_aws/processors/sqs_processor.py +250 -0
- qodalis_cli_aws/services/__init__.py +4 -0
- qodalis_cli_aws/services/aws_config_service.py +81 -0
- qodalis_cli_aws/services/aws_credential_manager.py +59 -0
- qodalis_cli_aws/utils/__init__.py +0 -0
- qodalis_cli_aws/utils/output_helpers.py +170 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/METADATA +25 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/RECORD +21 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from qodalis_cli_server_abstractions import (
|
|
9
|
+
get_positional_text,
|
|
10
|
+
CliCommandParameterDescriptor,
|
|
11
|
+
CliCommandProcessor,
|
|
12
|
+
CliProcessCommand,
|
|
13
|
+
ICliCommandParameterDescriptor,
|
|
14
|
+
ICliCommandProcessor,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
18
|
+
from ..utils.output_helpers import (
|
|
19
|
+
build_response,
|
|
20
|
+
build_error_response,
|
|
21
|
+
build_success_response,
|
|
22
|
+
format_as_list,
|
|
23
|
+
format_as_table,
|
|
24
|
+
apply_output_format,
|
|
25
|
+
is_dry_run,
|
|
26
|
+
CliServerTextOutput,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_s3_uri(uri: str) -> tuple[str, str] | None:
|
|
31
|
+
"""Parses an S3 URI into a (bucket, key) tuple.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
uri: An S3 URI in the form ``s3://bucket/key``.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A tuple of ``(bucket, key)``, or ``None`` if the URI is invalid.
|
|
38
|
+
"""
|
|
39
|
+
match = re.match(r"^s3://([^/]+)/?(.*)$", uri)
|
|
40
|
+
if not match:
|
|
41
|
+
return None
|
|
42
|
+
return match.group(1), match.group(2)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_bytes(num_bytes: int) -> str:
|
|
46
|
+
"""Formats a byte count into a human-readable string.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
num_bytes: Number of bytes.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Formatted string (e.g. ``"1.5 MB"``).
|
|
53
|
+
"""
|
|
54
|
+
if num_bytes == 0:
|
|
55
|
+
return "0 B"
|
|
56
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
57
|
+
i = 0
|
|
58
|
+
value = float(num_bytes)
|
|
59
|
+
while value >= 1024 and i < len(units) - 1:
|
|
60
|
+
value /= 1024
|
|
61
|
+
i += 1
|
|
62
|
+
return f"{value:.1f} {units[i]}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _S3LsProcessor(CliCommandProcessor):
|
|
66
|
+
"""Lists S3 buckets or objects within a bucket prefix."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
69
|
+
super().__init__()
|
|
70
|
+
self._credential_manager = credential_manager
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def command(self) -> str:
|
|
74
|
+
return "ls"
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def description(self) -> str:
|
|
78
|
+
return "List S3 buckets or objects in a bucket"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def value_required(self) -> bool | None:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
86
|
+
return [
|
|
87
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
88
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
95
|
+
"""Lists buckets when no value is given, or objects at the specified S3 URI."""
|
|
96
|
+
region = command.args.get("region")
|
|
97
|
+
client = self._credential_manager.get_client("s3", region=str(region) if region else None)
|
|
98
|
+
|
|
99
|
+
value = get_positional_text(command)
|
|
100
|
+
|
|
101
|
+
if not value:
|
|
102
|
+
try:
|
|
103
|
+
response = client.list_buckets()
|
|
104
|
+
buckets = response.get("Buckets", [])
|
|
105
|
+
|
|
106
|
+
if not buckets:
|
|
107
|
+
return build_response([CliServerTextOutput(value="No buckets found.", style="warning")])
|
|
108
|
+
|
|
109
|
+
items = []
|
|
110
|
+
for b in buckets:
|
|
111
|
+
date = b["CreationDate"].strftime("%Y-%m-%d") if b.get("CreationDate") else "(unknown)"
|
|
112
|
+
items.append(f"{date} {b.get('Name', '(unknown)')}")
|
|
113
|
+
|
|
114
|
+
default_output = format_as_list(items)
|
|
115
|
+
return build_response([apply_output_format(command, default_output, buckets)])
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
return build_error_response(f"Failed to list buckets: {exc}")
|
|
118
|
+
|
|
119
|
+
parsed = _parse_s3_uri(value)
|
|
120
|
+
if not parsed:
|
|
121
|
+
return build_error_response(f'Invalid S3 URI: "{value}". Expected format: s3://bucket[/prefix]')
|
|
122
|
+
|
|
123
|
+
bucket, prefix = parsed
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
paginator = client.get_paginator("list_objects_v2")
|
|
127
|
+
pages = paginator.paginate(Bucket=bucket, Prefix=prefix or "")
|
|
128
|
+
|
|
129
|
+
objects: list[dict[str, Any]] = []
|
|
130
|
+
for page in pages:
|
|
131
|
+
objects.extend(page.get("Contents", []))
|
|
132
|
+
|
|
133
|
+
if not objects:
|
|
134
|
+
return build_response([CliServerTextOutput(value=f"No objects found in s3://{bucket}/{prefix}", style="warning")])
|
|
135
|
+
|
|
136
|
+
rows: list[list[str]] = []
|
|
137
|
+
for obj in objects:
|
|
138
|
+
date = obj["LastModified"].strftime("%Y-%m-%d %H:%M:%S") if obj.get("LastModified") else "(unknown)"
|
|
139
|
+
size = _format_bytes(obj.get("Size", 0))
|
|
140
|
+
rows.append([date, size, obj.get("Key", "")])
|
|
141
|
+
|
|
142
|
+
table_output = format_as_table(["Last Modified", "Size", "Key"], rows)
|
|
143
|
+
return build_response([apply_output_format(command, table_output, objects)])
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
return build_error_response(f"Failed to list objects: {exc}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _S3CpProcessor(CliCommandProcessor):
|
|
149
|
+
"""Copies objects between S3 locations (S3-to-S3 only)."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
152
|
+
super().__init__()
|
|
153
|
+
self._credential_manager = credential_manager
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def command(self) -> str:
|
|
157
|
+
return "cp"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def description(self) -> str:
|
|
161
|
+
return "Copy objects between S3 locations (S3-to-S3 only)"
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def value_required(self) -> bool | None:
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
169
|
+
return [
|
|
170
|
+
CliCommandParameterDescriptor(name="dest", description="Destination S3 URI (s3://bucket/key)", required=True, type="string", aliases=["-d"]),
|
|
171
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
178
|
+
"""Copies an S3 object from the source URI to the destination URI."""
|
|
179
|
+
source = get_positional_text(command)
|
|
180
|
+
dest = command.args.get("dest")
|
|
181
|
+
|
|
182
|
+
if not source:
|
|
183
|
+
return build_error_response("Source S3 URI is required. Usage: s3 cp <s3://bucket/key> --dest <s3://bucket/key>")
|
|
184
|
+
|
|
185
|
+
if not dest:
|
|
186
|
+
return build_error_response("Destination is required. Use --dest <s3://bucket/key>.")
|
|
187
|
+
|
|
188
|
+
src_parsed = _parse_s3_uri(source)
|
|
189
|
+
if not src_parsed or not src_parsed[1]:
|
|
190
|
+
return build_error_response(f'Invalid source S3 URI: "{source}". Expected format: s3://bucket/key')
|
|
191
|
+
|
|
192
|
+
dst_parsed = _parse_s3_uri(str(dest))
|
|
193
|
+
if not dst_parsed or not dst_parsed[1]:
|
|
194
|
+
return build_error_response(f'Invalid destination S3 URI: "{dest}". Expected format: s3://bucket/key')
|
|
195
|
+
|
|
196
|
+
region = command.args.get("region")
|
|
197
|
+
client = self._credential_manager.get_client("s3", region=str(region) if region else None)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
client.copy_object(
|
|
201
|
+
Bucket=dst_parsed[0],
|
|
202
|
+
Key=dst_parsed[1],
|
|
203
|
+
CopySource=f"{src_parsed[0]}/{src_parsed[1]}",
|
|
204
|
+
)
|
|
205
|
+
return build_success_response(f"Copied {source} to {dest}")
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
return build_error_response(f"Failed to copy object: {exc}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class _S3RmProcessor(CliCommandProcessor):
|
|
211
|
+
"""Deletes an S3 object."""
|
|
212
|
+
|
|
213
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
214
|
+
super().__init__()
|
|
215
|
+
self._credential_manager = credential_manager
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def command(self) -> str:
|
|
219
|
+
return "rm"
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def description(self) -> str:
|
|
223
|
+
return "Delete an S3 object"
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def value_required(self) -> bool | None:
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
231
|
+
return [
|
|
232
|
+
CliCommandParameterDescriptor(name="dry-run", description="Preview without deleting", required=False, type="boolean"),
|
|
233
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
237
|
+
return ""
|
|
238
|
+
|
|
239
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
240
|
+
"""Deletes the specified S3 object, or previews the action in dry-run mode."""
|
|
241
|
+
value = get_positional_text(command)
|
|
242
|
+
if not value:
|
|
243
|
+
return build_error_response("S3 URI is required. Usage: s3 rm <s3://bucket/key>")
|
|
244
|
+
|
|
245
|
+
parsed = _parse_s3_uri(value)
|
|
246
|
+
if not parsed or not parsed[1]:
|
|
247
|
+
return build_error_response(f'Invalid S3 URI: "{value}". Expected format: s3://bucket/key')
|
|
248
|
+
|
|
249
|
+
if is_dry_run(command):
|
|
250
|
+
return build_response([CliServerTextOutput(value=f"[DRY RUN] Would delete {value}", style="warning")])
|
|
251
|
+
|
|
252
|
+
region = command.args.get("region")
|
|
253
|
+
client = self._credential_manager.get_client("s3", region=str(region) if region else None)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
client.delete_object(Bucket=parsed[0], Key=parsed[1])
|
|
257
|
+
return build_success_response(f"Deleted {value}")
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
return build_error_response(f"Failed to delete object: {exc}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class _S3MbProcessor(CliCommandProcessor):
|
|
263
|
+
"""Creates a new S3 bucket."""
|
|
264
|
+
|
|
265
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
266
|
+
super().__init__()
|
|
267
|
+
self._credential_manager = credential_manager
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def command(self) -> str:
|
|
271
|
+
return "mb"
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def description(self) -> str:
|
|
275
|
+
return "Create an S3 bucket"
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def value_required(self) -> bool | None:
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
283
|
+
return [
|
|
284
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
288
|
+
return ""
|
|
289
|
+
|
|
290
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
291
|
+
"""Creates a new S3 bucket with the specified name."""
|
|
292
|
+
bucket_name = get_positional_text(command)
|
|
293
|
+
if not bucket_name:
|
|
294
|
+
return build_error_response("Bucket name is required. Usage: s3 mb <bucket-name>")
|
|
295
|
+
|
|
296
|
+
region = command.args.get("region")
|
|
297
|
+
client = self._credential_manager.get_client("s3", region=str(region) if region else None)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
client.create_bucket(Bucket=bucket_name)
|
|
301
|
+
return build_success_response(f'Bucket "{bucket_name}" created successfully.')
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
return build_error_response(f"Failed to create bucket: {exc}")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class _S3RbProcessor(CliCommandProcessor):
|
|
307
|
+
"""Deletes an S3 bucket."""
|
|
308
|
+
|
|
309
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
310
|
+
super().__init__()
|
|
311
|
+
self._credential_manager = credential_manager
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def command(self) -> str:
|
|
315
|
+
return "rb"
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def description(self) -> str:
|
|
319
|
+
return "Delete an S3 bucket"
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def value_required(self) -> bool | None:
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
327
|
+
return [
|
|
328
|
+
CliCommandParameterDescriptor(name="dry-run", description="Preview without deleting", required=False, type="boolean"),
|
|
329
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
333
|
+
return ""
|
|
334
|
+
|
|
335
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
336
|
+
"""Deletes the specified bucket, or previews the action in dry-run mode."""
|
|
337
|
+
bucket_name = get_positional_text(command)
|
|
338
|
+
if not bucket_name:
|
|
339
|
+
return build_error_response("Bucket name is required. Usage: s3 rb <bucket-name>")
|
|
340
|
+
|
|
341
|
+
if is_dry_run(command):
|
|
342
|
+
return build_response([CliServerTextOutput(value=f'[DRY RUN] Would delete bucket "{bucket_name}"', style="warning")])
|
|
343
|
+
|
|
344
|
+
region = command.args.get("region")
|
|
345
|
+
client = self._credential_manager.get_client("s3", region=str(region) if region else None)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
client.delete_bucket(Bucket=bucket_name)
|
|
349
|
+
return build_success_response(f'Bucket "{bucket_name}" deleted successfully.')
|
|
350
|
+
except Exception as exc:
|
|
351
|
+
return build_error_response(f"Failed to delete bucket: {exc}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class _S3PresignProcessor(CliCommandProcessor):
|
|
355
|
+
"""Generates a pre-signed URL for an S3 object."""
|
|
356
|
+
|
|
357
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
358
|
+
super().__init__()
|
|
359
|
+
self._credential_manager = credential_manager
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def command(self) -> str:
|
|
363
|
+
return "presign"
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def description(self) -> str:
|
|
367
|
+
return "Generate a pre-signed URL for an S3 object"
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def value_required(self) -> bool | None:
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
375
|
+
return [
|
|
376
|
+
CliCommandParameterDescriptor(name="expires", description="URL expiration in seconds (default: 3600)", required=False, type="number", aliases=["-e"], default_value="3600"),
|
|
377
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
381
|
+
return ""
|
|
382
|
+
|
|
383
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
384
|
+
"""Generates and returns a time-limited pre-signed URL for the S3 object."""
|
|
385
|
+
value = get_positional_text(command)
|
|
386
|
+
if not value:
|
|
387
|
+
return build_error_response("S3 URI is required. Usage: s3 presign <s3://bucket/key>")
|
|
388
|
+
|
|
389
|
+
parsed = _parse_s3_uri(value)
|
|
390
|
+
if not parsed or not parsed[1]:
|
|
391
|
+
return build_error_response(f'Invalid S3 URI: "{value}". Expected format: s3://bucket/key')
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
expires_in = int(command.args.get("expires", 3600))
|
|
395
|
+
except (ValueError, TypeError):
|
|
396
|
+
return build_error_response("--expires must be a positive number of seconds.")
|
|
397
|
+
|
|
398
|
+
if expires_in <= 0:
|
|
399
|
+
return build_error_response("--expires must be a positive number of seconds.")
|
|
400
|
+
|
|
401
|
+
region = command.args.get("region")
|
|
402
|
+
client = self._credential_manager.get_client("s3", region=str(region) if region else None)
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
url = client.generate_presigned_url(
|
|
406
|
+
"get_object",
|
|
407
|
+
Params={"Bucket": parsed[0], "Key": parsed[1]},
|
|
408
|
+
ExpiresIn=expires_in,
|
|
409
|
+
)
|
|
410
|
+
return build_response([
|
|
411
|
+
CliServerTextOutput(value=url),
|
|
412
|
+
CliServerTextOutput(value=f"Expires in {expires_in} seconds.", style="info"),
|
|
413
|
+
])
|
|
414
|
+
except Exception as exc:
|
|
415
|
+
return build_error_response(f"Failed to generate pre-signed URL: {exc}")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class AwsS3Processor(CliCommandProcessor):
|
|
419
|
+
"""Parent processor for S3 sub-commands (ls, cp, rm, mb, rb, presign)."""
|
|
420
|
+
|
|
421
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
422
|
+
super().__init__()
|
|
423
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
424
|
+
_S3LsProcessor(credential_manager),
|
|
425
|
+
_S3CpProcessor(credential_manager),
|
|
426
|
+
_S3RmProcessor(credential_manager),
|
|
427
|
+
_S3MbProcessor(credential_manager),
|
|
428
|
+
_S3RbProcessor(credential_manager),
|
|
429
|
+
_S3PresignProcessor(credential_manager),
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def command(self) -> str:
|
|
434
|
+
return "s3"
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def description(self) -> str:
|
|
438
|
+
return "Amazon S3 operations -- list, copy, remove objects and buckets"
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
442
|
+
"""Returns sub-processors for S3 operations."""
|
|
443
|
+
return self._sub_processors
|
|
444
|
+
|
|
445
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
446
|
+
return ""
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
build_success_response,
|
|
21
|
+
format_as_table,
|
|
22
|
+
format_as_list,
|
|
23
|
+
apply_output_format,
|
|
24
|
+
CliServerTextOutput,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _SnsTopicsProcessor(CliCommandProcessor):
|
|
29
|
+
"""Lists all SNS topics."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self._credential_manager = credential_manager
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def command(self) -> str:
|
|
37
|
+
return "topics"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def description(self) -> str:
|
|
41
|
+
return "List SNS topics"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
45
|
+
return [
|
|
46
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
47
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
54
|
+
"""Fetches and returns all SNS topic ARNs."""
|
|
55
|
+
region = command.args.get("region")
|
|
56
|
+
client = self._credential_manager.get_client("sns", region=str(region) if region else None)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
response = client.list_topics()
|
|
60
|
+
topics = response.get("Topics", [])
|
|
61
|
+
|
|
62
|
+
if not topics:
|
|
63
|
+
return build_response([CliServerTextOutput(value="No SNS topics found.", style="warning")])
|
|
64
|
+
|
|
65
|
+
arns = [t.get("TopicArn", "(unknown)") for t in topics]
|
|
66
|
+
list_output = format_as_list(arns)
|
|
67
|
+
return build_response([apply_output_format(command, list_output, topics)])
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
return build_error_response(f"Failed to list SNS topics: {exc}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _SnsPublishProcessor(CliCommandProcessor):
|
|
73
|
+
"""Publishes a message to an SNS topic."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
76
|
+
super().__init__()
|
|
77
|
+
self._credential_manager = credential_manager
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def command(self) -> str:
|
|
81
|
+
return "publish"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def description(self) -> str:
|
|
85
|
+
return "Publish a message to an SNS topic"
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def value_required(self) -> bool | None:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
93
|
+
return [
|
|
94
|
+
CliCommandParameterDescriptor(name="message", description="Message to publish", required=True, type="string", aliases=["-m"]),
|
|
95
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
102
|
+
"""Publishes the provided message to the specified SNS topic ARN."""
|
|
103
|
+
topic_arn = get_positional_text(command)
|
|
104
|
+
if not topic_arn:
|
|
105
|
+
return build_error_response("Topic ARN is required. Usage: sns publish <topic-arn> --message <message>")
|
|
106
|
+
|
|
107
|
+
message = command.args.get("message")
|
|
108
|
+
if not message:
|
|
109
|
+
return build_error_response("--message is required. Usage: sns publish <topic-arn> --message <message>")
|
|
110
|
+
|
|
111
|
+
region = command.args.get("region")
|
|
112
|
+
client = self._credential_manager.get_client("sns", region=str(region) if region else None)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
response = client.publish(TopicArn=topic_arn, Message=str(message))
|
|
116
|
+
return build_success_response(f"Message published successfully. MessageId: {response.get('MessageId', '(unknown)')}")
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
return build_error_response(f"Failed to publish message: {exc}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _SnsSubscriptionsProcessor(CliCommandProcessor):
|
|
122
|
+
"""Lists SNS subscriptions, optionally filtered by topic ARN."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
125
|
+
super().__init__()
|
|
126
|
+
self._credential_manager = credential_manager
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def command(self) -> str:
|
|
130
|
+
return "subscriptions"
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def description(self) -> str:
|
|
134
|
+
return "List SNS subscriptions (optionally filtered by topic ARN)"
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
138
|
+
return [
|
|
139
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
140
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
144
|
+
return ""
|
|
145
|
+
|
|
146
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
147
|
+
"""Fetches subscriptions for a specific topic or all subscriptions."""
|
|
148
|
+
topic_arn = get_positional_text(command) or None
|
|
149
|
+
|
|
150
|
+
region = command.args.get("region")
|
|
151
|
+
client = self._credential_manager.get_client("sns", region=str(region) if region else None)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
if topic_arn:
|
|
155
|
+
response = client.list_subscriptions_by_topic(TopicArn=topic_arn)
|
|
156
|
+
else:
|
|
157
|
+
response = client.list_subscriptions()
|
|
158
|
+
|
|
159
|
+
subscriptions = response.get("Subscriptions", [])
|
|
160
|
+
|
|
161
|
+
if not subscriptions:
|
|
162
|
+
return build_response([CliServerTextOutput(value="No SNS subscriptions found.", style="warning")])
|
|
163
|
+
|
|
164
|
+
rows = [
|
|
165
|
+
[
|
|
166
|
+
sub.get("SubscriptionArn", "(unknown)"),
|
|
167
|
+
sub.get("Protocol", "(unknown)"),
|
|
168
|
+
sub.get("Endpoint", "(unknown)"),
|
|
169
|
+
sub.get("TopicArn", "(unknown)"),
|
|
170
|
+
]
|
|
171
|
+
for sub in subscriptions
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
table_output = format_as_table(["SubscriptionArn", "Protocol", "Endpoint", "TopicArn"], rows)
|
|
175
|
+
return build_response([apply_output_format(command, table_output, subscriptions)])
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
return build_error_response(f"Failed to list SNS subscriptions: {exc}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class AwsSnsProcessor(CliCommandProcessor):
|
|
181
|
+
"""Parent processor for SNS sub-commands (topics, publish, subscriptions)."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
184
|
+
super().__init__()
|
|
185
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
186
|
+
_SnsTopicsProcessor(credential_manager),
|
|
187
|
+
_SnsPublishProcessor(credential_manager),
|
|
188
|
+
_SnsSubscriptionsProcessor(credential_manager),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def command(self) -> str:
|
|
193
|
+
return "sns"
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def description(self) -> str:
|
|
197
|
+
return "AWS SNS operations -- topics, publish, subscriptions"
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
201
|
+
"""Returns sub-processors for SNS operations."""
|
|
202
|
+
return self._sub_processors
|
|
203
|
+
|
|
204
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
205
|
+
return ""
|