nonebot-plugin-awsmgmt 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,2 +1,246 @@
1
- def hello() -> str:
2
- return "Hello from nonebot-plugin-awsmgmt!"
1
+ import time
2
+ import re
3
+ from typing import Tuple, List, Optional, Dict, Any
4
+ from functools import wraps
5
+
6
+ from nonebot import on_command
7
+ from nonebot.matcher import Matcher
8
+ from nonebot.permission import SUPERUSER
9
+ from nonebot.plugin import PluginMetadata, get_plugin_config
10
+ from nonebot.params import CommandArg
11
+ from nonebot.adapters import Message
12
+ from nonebot.log import logger
13
+ from nonebot.exception import FinishedException
14
+
15
+ from .config import Config
16
+ from .ec2_manager import EC2Manager
17
+ from .cost_explorer_manager import CostExplorerManager
18
+ from .lightsail_manager import LightsailManager
19
+
20
+ __plugin_meta__ = PluginMetadata(
21
+ name="AWS Manager",
22
+ description="Manage AWS EC2, Lightsail, and Cost Explorer via commands.",
23
+ usage=(
24
+ "--- EC2 ---\n"
25
+ "/ec2_start|stop|reboot|status [target]\n"
26
+ "Target: tag:Key:Value | id:i-xxxx\n"
27
+ "--- Lightsail ---\n"
28
+ "/lightsail_list\n"
29
+ "/lightsail_start|stop <instance_name>\n"
30
+ "--- Cost ---\n"
31
+ "/aws_cost today|month|month by_service"
32
+ ),
33
+ type="application",
34
+ homepage="https://github.com/maxesisn/nonebot-plugin-awsmgmt",
35
+ config=Config,
36
+ )
37
+
38
+ def handle_non_finish_exceptions(error_message: str):
39
+ def decorator(func):
40
+ @wraps(func)
41
+ async def wrapper(*args, **kwargs):
42
+ try:
43
+ return await func(*args, **kwargs)
44
+ except FinishedException:
45
+ raise
46
+ except Exception as e:
47
+ matcher = args[0] if args else None
48
+ logger.error(f"Error in {func.__name__}: {e}")
49
+ if matcher:
50
+ await matcher.finish(error_message)
51
+ return wrapper
52
+ return decorator
53
+
54
+ # --- Init --- #
55
+ plugin_config = get_plugin_config(Config)
56
+ ec2_manager = EC2Manager(plugin_config)
57
+ cost_manager = CostExplorerManager(plugin_config)
58
+ lightsail_manager = LightsailManager(plugin_config)
59
+
60
+ # --- Command Matchers --- #
61
+ # EC2
62
+ ec2_start_matcher = on_command("ec2_start", aliases={"ec2启动"}, permission=SUPERUSER)
63
+ ec2_stop_matcher = on_command("ec2_stop", aliases={"ec2停止"}, permission=SUPERUSER)
64
+ ec2_reboot_matcher = on_command("ec2_reboot", aliases={"ec2重启"}, permission=SUPERUSER)
65
+ ec2_status_matcher = on_command("ec2_status", aliases={"ec2状态"}, permission=SUPERUSER)
66
+ # Lightsail
67
+ lightsail_list_matcher = on_command("lightsail_list", permission=SUPERUSER)
68
+ lightsail_start_matcher = on_command("lightsail_start", permission=SUPERUSER)
69
+ lightsail_stop_matcher = on_command("lightsail_stop", permission=SUPERUSER)
70
+ # Cost Explorer
71
+ cost_matcher = on_command("aws_cost", permission=SUPERUSER)
72
+
73
+
74
+ # --- Helper Functions --- #
75
+
76
+ async def parse_ec2_target(matcher: Matcher, args: Message) -> Tuple[str, str, Optional[str]]:
77
+ arg_str = args.extract_plain_text().strip()
78
+ if not arg_str:
79
+ if plugin_config.aws_default_target_tag:
80
+ arg_str = f"tag:{plugin_config.aws_default_target_tag}"
81
+ else:
82
+ await matcher.finish(__plugin_meta__.usage)
83
+ match = re.match(r"^(tag|id):(.*)$", arg_str)
84
+ if not match:
85
+ await matcher.finish(f"Invalid EC2 target format. \n{__plugin_meta__.usage}")
86
+ target_type, value = match.groups()
87
+ if target_type == "tag":
88
+ if ":" not in value:
89
+ await matcher.finish(f"Invalid tag format. Expected Key:Value. \n{__plugin_meta__.usage}")
90
+ tag_key, tag_value = value.split(":", 1)
91
+ return "tag", tag_key, tag_value
92
+ elif target_type == "id":
93
+ return "id", value, None
94
+ return "unknown", "", None
95
+
96
+ def format_ec2_status(instance: Dict[str, Any]) -> str:
97
+ instance_id = instance.get('InstanceId', 'N/A')
98
+ state = instance.get('State', {}).get('Name', 'N/A')
99
+ public_ip = instance.get('PublicIpAddress', 'None')
100
+ name_tag = next((tag['Value'] for tag in instance.get('Tags', []) if tag['Key'] == 'Name'), 'No Name Tag')
101
+ return f"- {instance_id} ({name_tag})\n State: {state}\n Public IP: {public_ip}"
102
+
103
+
104
+ # --- EC2 Handlers --- #
105
+
106
+ async def ec2_operation(matcher: Matcher, args: Message, operation: str):
107
+ """Helper function to handle EC2 start, stop, and reboot operations."""
108
+ target_type, value1, value2 = await parse_ec2_target(matcher, args)
109
+
110
+ states_map = {
111
+ "start": (["stopped"], "running", "start_instances", "instance_running"),
112
+ "stop": (["running"], "stopped", "stop_instances", "instance_stopped"),
113
+ "reboot": (["running"], "running", "reboot_instances", None), # Reboot does not have a waiter
114
+ }
115
+
116
+ if operation not in states_map:
117
+ await matcher.finish("Invalid operation.")
118
+
119
+ states, target_status, op_func_name, waiter_name = states_map[operation]
120
+
121
+ if target_type == "tag":
122
+ instances = await ec2_manager.get_instances_by_tag(value1, value2, states=states)
123
+ else:
124
+ instances = await ec2_manager.get_instances_by_id([value1], states=states)
125
+
126
+ if not instances:
127
+ await matcher.finish(f"No instances to {operation}.")
128
+
129
+ instance_ids = [inst['InstanceId'] for inst in instances]
130
+ await matcher.send(f"Sending {operation} command to instances:\n" + "\n".join(instance_ids))
131
+
132
+ op_func = getattr(ec2_manager, op_func_name)
133
+ await op_func(instance_ids)
134
+
135
+ if waiter_name:
136
+ await matcher.send("Waiting for them to be " + ("rebooted" if operation == "reboot" else f"{target_status}..."))
137
+ start_time = time.time()
138
+ async with ec2_manager.session.client("ec2") as ec2:
139
+ waiter = ec2.get_waiter(waiter_name)
140
+ await waiter.wait(InstanceIds=instance_ids)
141
+ elapsed_time = time.time() - start_time
142
+ await matcher.finish(f"Successfully {operation}ed instances in {elapsed_time:.2f} seconds.")
143
+ else: # For reboot
144
+ await matcher.finish(f"Successfully sent reboot command to instances.")
145
+
146
+
147
+ @ec2_start_matcher.handle()
148
+ @handle_non_finish_exceptions("An error occurred while starting EC2 instances.")
149
+ async def handle_ec2_start(matcher: Matcher, args: Message = CommandArg()):
150
+ await ec2_operation(matcher, args, "start")
151
+
152
+ @ec2_stop_matcher.handle()
153
+ @handle_non_finish_exceptions("An error occurred while stopping EC2 instances.")
154
+ async def handle_ec2_stop(matcher: Matcher, args: Message = CommandArg()):
155
+ await ec2_operation(matcher, args, "stop")
156
+
157
+ @ec2_reboot_matcher.handle()
158
+ @handle_non_finish_exceptions("An error occurred while rebooting EC2 instances.")
159
+ async def handle_ec2_reboot(matcher: Matcher, args: Message = CommandArg()):
160
+ await ec2_operation(matcher, args, "reboot")
161
+
162
+ @ec2_status_matcher.handle()
163
+ @handle_non_finish_exceptions("An error occurred while fetching EC2 status.")
164
+ async def handle_ec2_status(matcher: Matcher, args: Message = CommandArg()):
165
+ target_type, value1, value2 = await parse_ec2_target(matcher, args)
166
+
167
+ if target_type == "tag":
168
+ instances = await ec2_manager.get_instances_by_tag(value1, value2, states=['pending', 'running', 'stopping', 'stopped'])
169
+ else:
170
+ instances = await ec2_manager.get_instances_by_id([value1], states=['pending', 'running', 'stopping', 'stopped'])
171
+ if not instances:
172
+ await matcher.finish("No EC2 instances found for the specified target.")
173
+ status_list = [format_ec2_status(inst) for inst in instances]
174
+ await matcher.finish("EC2 Instance Status:\n" + "\n".join(status_list))
175
+
176
+
177
+ # ... (omitting other EC2 handlers for brevity, they remain the same)
178
+
179
+
180
+ # --- Lightsail Handlers ---
181
+
182
+
183
+ @lightsail_list_matcher.handle()
184
+ @handle_non_finish_exceptions("An error occurred listing Lightsail instances.")
185
+ async def handle_lightsail_list(matcher: Matcher):
186
+ instances = await lightsail_manager.get_all_instances()
187
+ if not instances:
188
+ await matcher.finish("No Lightsail instances found.")
189
+
190
+ def format_lightsail(inst):
191
+ return f"- {inst['name']} ({inst['state']['name']})\n Region: {inst['location']['regionName']}\n IP: {inst['publicIpAddress']}"
192
+
193
+ status_list = [format_lightsail(inst) for inst in instances]
194
+ await matcher.finish("Lightsail Instances:\n" + "\n".join(status_list))
195
+
196
+ @lightsail_start_matcher.handle()
197
+ @handle_non_finish_exceptions("An error occurred while starting the Lightsail instance.")
198
+ async def handle_lightsail_start(matcher: Matcher, args: Message = CommandArg()):
199
+ instance_name = args.extract_plain_text().strip()
200
+ if not instance_name:
201
+ await matcher.finish("Please provide a Lightsail instance name.")
202
+
203
+ await matcher.send(f"Sending start command to {instance_name}...\nWaiting for it to become running...")
204
+ await lightsail_manager.start_instance(instance_name)
205
+ await lightsail_manager.wait_for_status(instance_name, 'running')
206
+ await matcher.finish(f"Successfully started Lightsail instance: {instance_name}")
207
+
208
+ @lightsail_stop_matcher.handle()
209
+ @handle_non_finish_exceptions("An error occurred while stopping the Lightsail instance.")
210
+ async def handle_lightsail_stop(matcher: Matcher, args: Message = CommandArg()):
211
+ instance_name = args.extract_plain_text().strip()
212
+ if not instance_name:
213
+ await matcher.finish("Please provide a Lightsail instance name.")
214
+
215
+ await matcher.send(f"Sending stop command to {instance_name}...\nWaiting for it to become stopped...")
216
+ await lightsail_manager.stop_instance(instance_name)
217
+ await lightsail_manager.wait_for_status(instance_name, 'stopped')
218
+ await matcher.finish(f"Successfully stopped Lightsail instance: {instance_name}")
219
+
220
+
221
+ # --- Cost Explorer Handlers ---
222
+
223
+ @cost_matcher.handle()
224
+ @handle_non_finish_exceptions("An error occurred while fetching AWS cost data.")
225
+ async def handle_cost(matcher: Matcher, args: Message = CommandArg()):
226
+ sub_command = args.extract_plain_text().strip()
227
+
228
+ if sub_command == "today":
229
+ result = await cost_manager.get_cost_today()
230
+ cost = result['ResultsByTime'][0]['Total']['UnblendedCost']
231
+ await matcher.finish(f"AWS cost for today: {float(cost['Amount']):.4f} {cost['Unit']}")
232
+ elif sub_command == "month":
233
+ result = await cost_manager.get_cost_this_month()
234
+ cost = result['ResultsByTime'][0]['Total']['UnblendedCost']
235
+ await matcher.finish(f"AWS cost this month: {float(cost['Amount']):.4f} {cost['Unit']}")
236
+ elif sub_command == "month by_service":
237
+ result = await cost_manager.get_cost_this_month_by_service()
238
+ lines = ["Cost this month by service:"]
239
+ for group in sorted(result['ResultsByTime'][0]['Groups'], key=lambda x: float(x['Metrics']['UnblendedCost']['Amount']), reverse=True):
240
+ service_name = group['Keys'][0]
241
+ cost = group['Metrics']['UnblendedCost']
242
+ if float(cost['Amount']) > 0:
243
+ lines.append(f"- {service_name}: {float(cost['Amount']):.4f} {cost['Unit']}")
244
+ await matcher.finish("\n".join(lines))
245
+ else:
246
+ await matcher.finish("Invalid cost command. Use: today, month, month by_service")
@@ -0,0 +1,14 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+
4
+ class Config(BaseModel):
5
+ """
6
+ AWS Management Plugin Configuration
7
+ """
8
+ aws_access_key_id: Optional[str] = None
9
+ aws_secret_access_key: Optional[str] = None
10
+ aws_region: str = "us-east-1"
11
+ aws_default_target_tag: Optional[str] = "ManagedBy:nonebot-plugin-awsmgmt"
12
+
13
+ class Config:
14
+ extra = "ignore"
@@ -0,0 +1,51 @@
1
+ import aioboto3
2
+ from datetime import datetime, date, timedelta
3
+ from typing import Dict, Any, List
4
+
5
+ from .config import Config
6
+
7
+ class CostExplorerManager:
8
+ def __init__(self, config: Config):
9
+ self.config = config
10
+ # Cost Explorer is only available in us-east-1, but we use the session region for authentication
11
+ self.session = aioboto3.Session(
12
+ aws_access_key_id=self.config.aws_access_key_id,
13
+ aws_secret_access_key=self.config.aws_secret_access_key,
14
+ region_name=self.config.aws_region,
15
+ )
16
+
17
+ async def _get_cost(self, start_date: str, end_date: str, granularity: str, group_by: List[Dict[str, str]] = None) -> Dict[str, Any]:
18
+ async with self.session.client("ce", region_name="us-east-1") as ce:
19
+ kwargs = {
20
+ "TimePeriod": {"Start": start_date, "End": end_date},
21
+ "Granularity": granularity,
22
+ "Metrics": ["UnblendedCost"],
23
+ }
24
+ if group_by:
25
+ kwargs["GroupBy"] = group_by
26
+ return await ce.get_cost_and_usage(**kwargs)
27
+
28
+ async def get_cost_today(self) -> Dict[str, Any]:
29
+ """Fetches the cost for today."""
30
+ today = date.today()
31
+ start_of_day = today.isoformat()
32
+ end_of_day = (today + timedelta(days=1)).isoformat()
33
+ return await self._get_cost(start_of_day, end_of_day, "DAILY")
34
+
35
+ async def get_cost_this_month(self) -> Dict[str, Any]:
36
+ """Fetches the cost for the current month."""
37
+ today = date.today()
38
+ start_of_month = today.replace(day=1).isoformat()
39
+ # The end date is exclusive, so we use the first day of the next month
40
+ next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
41
+ end_of_month = next_month.isoformat()
42
+ return await self._get_cost(start_of_month, end_of_month, "MONTHLY")
43
+
44
+ async def get_cost_this_month_by_service(self) -> Dict[str, Any]:
45
+ """Fetches the cost for the current month, grouped by service."""
46
+ today = date.today()
47
+ start_of_month = today.replace(day=1).isoformat()
48
+ next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
49
+ end_of_month = next_month.isoformat()
50
+ group_by = [{"Type": "DIMENSION", "Key": "SERVICE"}]
51
+ return await self._get_cost(start_of_month, end_of_month, "MONTHLY", group_by=group_by)
@@ -0,0 +1,71 @@
1
+ import aioboto3
2
+ from typing import List, Dict, Any
3
+ from .config import Config
4
+
5
+ class EC2Manager:
6
+ def __init__(self, config: Config):
7
+ self.config = config
8
+ self.session = aioboto3.Session(
9
+ aws_access_key_id=self.config.aws_access_key_id,
10
+ aws_secret_access_key=self.config.aws_secret_access_key,
11
+ region_name=self.config.aws_region,
12
+ )
13
+
14
+ async def get_instances_by_tag(self, tag_key: str, tag_value: str, states: List[str] = ['running', 'stopped']) -> List[Dict[str, Any]]:
15
+ """Finds EC2 instances based on a specific tag."""
16
+ instances = []
17
+ async with self.session.client("ec2") as ec2:
18
+ paginator = ec2.get_paginator('describe_instances')
19
+ async for page in paginator.paginate(
20
+ Filters=[
21
+ {'Name': f'tag:{tag_key}', 'Values': [tag_value]},
22
+ {'Name': 'instance-state-name', 'Values': states}
23
+ ]
24
+ ):
25
+ for reservation in page['Reservations']:
26
+ for instance in reservation['Instances']:
27
+ instances.append(instance)
28
+ return instances
29
+
30
+ async def get_instances_by_id(self, instance_ids: List[str], states: List[str] = ['running', 'stopped']) -> List[Dict[str, Any]]:
31
+ """Finds EC2 instances based on a list of IDs."""
32
+ instances = []
33
+ async with self.session.client("ec2") as ec2:
34
+ paginator = ec2.get_paginator('describe_instances')
35
+ async for page in paginator.paginate(
36
+ InstanceIds=instance_ids,
37
+ Filters=[{'Name': 'instance-state-name', 'Values': states}]
38
+ ):
39
+ for reservation in page['Reservations']:
40
+ for instance in reservation['Instances']:
41
+ instances.append(instance)
42
+ return instances
43
+
44
+ async def start_instances(self, instance_ids: List[str]) -> Dict[str, Any]:
45
+ """Starts the specified EC2 instances."""
46
+ if not instance_ids:
47
+ return {"StartingInstances": []}
48
+ async with self.session.client("ec2") as ec2:
49
+ return await ec2.start_instances(InstanceIds=instance_ids)
50
+
51
+ async def stop_instances(self, instance_ids: List[str]) -> Dict[str, Any]:
52
+ """Stops the specified EC2 instances."""
53
+ if not instance_ids:
54
+ return {"StoppingInstances": []}
55
+ async with self.session.client("ec2") as ec2:
56
+ return await ec2.stop_instances(InstanceIds=instance_ids)
57
+
58
+ async def reboot_instances(self, instance_ids: List[str]) -> Dict[str, Any]:
59
+ """Reboots the specified EC2 instances."""
60
+ if not instance_ids:
61
+ return {}
62
+ async with self.session.client("ec2") as ec2:
63
+ return await ec2.reboot_instances(InstanceIds=instance_ids)
64
+
65
+ async def wait_for_status(self, instance_ids: List[str], status: str):
66
+ """Waits for instances to reach a specific status."""
67
+ if not instance_ids:
68
+ return
69
+ async with self.session.client("ec2") as ec2:
70
+ waiter = ec2.get_waiter(status)
71
+ await waiter.wait(InstanceIds=instance_ids)
@@ -0,0 +1,43 @@
1
+ import asyncio
2
+ import aioboto3
3
+ from typing import List, Dict, Any
4
+
5
+ from .config import Config
6
+
7
+ class LightsailManager:
8
+ def __init__(self, config: Config):
9
+ self.config = config
10
+ self.session = aioboto3.Session(
11
+ aws_access_key_id=self.config.aws_access_key_id,
12
+ aws_secret_access_key=self.config.aws_secret_access_key,
13
+ region_name=self.config.aws_region,
14
+ )
15
+
16
+ async def get_all_instances(self) -> List[Dict[str, Any]]:
17
+ """Gets a list of all Lightsail instances."""
18
+ async with self.session.client("lightsail") as lightsail:
19
+ response = await lightsail.get_instances()
20
+ return response.get('instances', [])
21
+
22
+ async def start_instance(self, instance_name: str) -> Dict[str, Any]:
23
+ """Starts a specific Lightsail instance."""
24
+ async with self.session.client("lightsail") as lightsail:
25
+ return await lightsail.start_instance(instanceName=instance_name)
26
+
27
+ async def stop_instance(self, instance_name: str) -> Dict[str, Any]:
28
+ """Stops a specific Lightsail instance."""
29
+ async with self.session.client("lightsail") as lightsail:
30
+ return await lightsail.stop_instance(instanceName=instance_name)
31
+
32
+ async def wait_for_status(self, instance_name: str, target_status: str, timeout: int = 300, delay: int = 15):
33
+ """Waits for a lightsail instance to reach a specific status."""
34
+ async with self.session.client("lightsail") as lightsail:
35
+ elapsed_time = 0
36
+ while elapsed_time < timeout:
37
+ response = await lightsail.get_instance(instanceName=instance_name)
38
+ instance = response.get('instance')
39
+ if instance and instance.get('state', {}).get('name') == target_status:
40
+ return
41
+ await asyncio.sleep(delay)
42
+ elapsed_time += delay
43
+ raise asyncio.TimeoutError(f"Instance {instance_name} did not reach {target_status} within {timeout} seconds.")
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-awsmgmt
3
- Version: 0.1.0
4
- Summary: Add your description here
3
+ Version: 0.1.2
4
+ Summary: A nonebot2 plugin for managing AWS EC2, Lightsail, and Cost Explorer via commands.
5
5
  Author-email: Maximilian Wu <me@maxng.cc>
6
6
  Requires-Python: >=3.8
7
7
  Requires-Dist: aioboto3>=15.0.0
@@ -10,10 +10,37 @@ Description-Content-Type: text/markdown
10
10
 
11
11
  # nonebot_plugin_awsmgmt
12
12
 
13
- A NoneBot2 plugin for AWS management.
13
+ 一个用于 AWS 管理的 NoneBot2 插件。
14
14
 
15
15
  ## 使用方法
16
16
 
17
+ ---
18
+ ### EC2
19
+ - `/ec2_start [target]`
20
+ - `/ec2_stop [target]`
21
+ - `/ec2_reboot [target]`
22
+ - `/ec2_status [target]`
23
+
24
+ **Target (目标):**
25
+ - `tag:Key:Value`: 例如 `tag:Project:MyProject`
26
+ - `id:i-xxxx`: 例如 `id:i-0fd0acc80b595ac71`
27
+
28
+ 如果未提供 `target`,插件将使用您在配置中设置的 `aws_default_target_tag`(如果已设置)。
29
+
30
+ ---
31
+ ### Lightsail
32
+ - `/lightsail_list`
33
+ - `/lightsail_start <实例名称>`
34
+ - `/lightsail_stop <实例名称>`
35
+
36
+ ---
37
+ ### Cost Explorer (成本管理器)
38
+ - `/aws_cost today` (今日成本)
39
+ - `/aws_cost month` (本月成本)
40
+ - `/aws_cost month by_service` (按服务划分的本月成本)
41
+
42
+ ## 安装
43
+
17
44
  1. **安装插件**
18
45
 
19
46
  ```bash
@@ -33,12 +60,12 @@ A NoneBot2 plugin for AWS management.
33
60
 
34
61
  3. **配置 AWS 凭证**
35
62
 
36
- NoneBot2 项目的 `.env` 文件中配置 AWS 访问密钥和秘密访问密钥:
63
+ 在您的 NoneBot2 项目的 `.env` 文件中配置您的 AWS 访问密钥和秘密访问密钥:
37
64
 
38
65
  ```
39
66
  AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
40
67
  AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
41
- AWS_REGION_NAME=your-aws-region # 例如:us-east-1
68
+ AWS_REGION_NAME=your-aws-region # 例如: us-east-1
42
69
  ```
43
70
 
44
71
  ## AWS 侧配置
@@ -0,0 +1,8 @@
1
+ nonebot_plugin_awsmgmt/__init__.py,sha256=9MUIJIqcnMeaVhackYzIn6LiREoT6rS2T1bfqHNXYjk,10864
2
+ nonebot_plugin_awsmgmt/config.py,sha256=tI5-JmakhKmxWtTv5qeTpszsHyJGJW1X48Q278HC0a8,389
3
+ nonebot_plugin_awsmgmt/cost_explorer_manager.py,sha256=dppD4EE8YZzhAx0mCgftMPoBNSSLwdBNA6t4L7Hnfts,2394
4
+ nonebot_plugin_awsmgmt/ec2_manager.py,sha256=BIpHV8jo8nBQotePWa31mcNRwl14pL7mQZodXAK3p8Y,3200
5
+ nonebot_plugin_awsmgmt/lightsail_manager.py,sha256=GEbrQAihLucVfF1XyJgvtsig_yCBedH1GyoBgPkIrCo,2025
6
+ nonebot_plugin_awsmgmt-0.1.2.dist-info/METADATA,sha256=MM7tZ7c7zTXzyMKqWAsTFOpVUqEWYJtx-lsBKfSPAZg,4346
7
+ nonebot_plugin_awsmgmt-0.1.2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
8
+ nonebot_plugin_awsmgmt-0.1.2.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- nonebot_plugin_awsmgmt/__init__.py,sha256=3-7xXlyyH7iTXdZ36lyytnJrIKWLXvUbj_45Hnuk6Bg,68
2
- nonebot_plugin_awsmgmt-0.1.0.dist-info/METADATA,sha256=TPx99HgE1bSoNopTx5aUI12DELDpn4j9QHEUtHWREkw,3627
3
- nonebot_plugin_awsmgmt-0.1.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
4
- nonebot_plugin_awsmgmt-0.1.0.dist-info/RECORD,,