nonebot-plugin-awsmgmt 0.1.0__py3-none-any.whl → 0.1.1__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.
- nonebot_plugin_awsmgmt/__init__.py +189 -2
- nonebot_plugin_awsmgmt/config.py +14 -0
- nonebot_plugin_awsmgmt/cost_explorer_manager.py +51 -0
- nonebot_plugin_awsmgmt/ec2_manager.py +71 -0
- nonebot_plugin_awsmgmt/lightsail_manager.py +43 -0
- {nonebot_plugin_awsmgmt-0.1.0.dist-info → nonebot_plugin_awsmgmt-0.1.1.dist-info}/METADATA +2 -2
- nonebot_plugin_awsmgmt-0.1.1.dist-info/RECORD +8 -0
- nonebot_plugin_awsmgmt-0.1.0.dist-info/RECORD +0 -4
- {nonebot_plugin_awsmgmt-0.1.0.dist-info → nonebot_plugin_awsmgmt-0.1.1.dist-info}/WHEEL +0 -0
@@ -1,2 +1,189 @@
|
|
1
|
-
|
2
|
-
|
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
|
+
@ec2_status_matcher.handle()
|
107
|
+
@handle_non_finish_exceptions("An error occurred while fetching EC2 status.")
|
108
|
+
async def handle_ec2_status(matcher: Matcher, args: Message = CommandArg()):
|
109
|
+
target_type, value1, value2 = await parse_ec2_target(matcher, args)
|
110
|
+
|
111
|
+
if target_type == "tag":
|
112
|
+
instances = await ec2_manager.get_instances_by_tag(value1, value2, states=['pending', 'running', 'stopping', 'stopped'])
|
113
|
+
else:
|
114
|
+
instances = await ec2_manager.get_instances_by_id([value1], states=['pending', 'running', 'stopping', 'stopped'])
|
115
|
+
if not instances:
|
116
|
+
await matcher.finish("No EC2 instances found for the specified target.")
|
117
|
+
status_list = [format_ec2_status(inst) for inst in instances]
|
118
|
+
await matcher.finish("EC2 Instance Status:\n" + "\n".join(status_list))
|
119
|
+
|
120
|
+
|
121
|
+
# ... (omitting other EC2 handlers for brevity, they remain the same)
|
122
|
+
|
123
|
+
|
124
|
+
# --- Lightsail Handlers ---
|
125
|
+
|
126
|
+
@lightsail_list_matcher.handle()
|
127
|
+
@handle_non_finish_exceptions("An error occurred listing Lightsail instances.")
|
128
|
+
async def handle_lightsail_list(matcher: Matcher):
|
129
|
+
instances = await lightsail_manager.get_all_instances()
|
130
|
+
if not instances:
|
131
|
+
await matcher.finish("No Lightsail instances found.")
|
132
|
+
|
133
|
+
def format_lightsail(inst):
|
134
|
+
return f"- {inst['name']} ({inst['state']['name']})\n Region: {inst['location']['regionName']}\n IP: {inst['publicIpAddress']}"
|
135
|
+
|
136
|
+
status_list = [format_lightsail(inst) for inst in instances]
|
137
|
+
await matcher.finish("Lightsail Instances:\n" + "\n".join(status_list))
|
138
|
+
|
139
|
+
@lightsail_start_matcher.handle()
|
140
|
+
@handle_non_finish_exceptions("An error occurred while starting the Lightsail instance.")
|
141
|
+
async def handle_lightsail_start(matcher: Matcher, args: Message = CommandArg()):
|
142
|
+
instance_name = args.extract_plain_text().strip()
|
143
|
+
if not instance_name:
|
144
|
+
await matcher.finish("Please provide a Lightsail instance name.")
|
145
|
+
|
146
|
+
await matcher.send(f"Sending start command to {instance_name}...\nWaiting for it to become running...")
|
147
|
+
await lightsail_manager.start_instance(instance_name)
|
148
|
+
await lightsail_manager.wait_for_status(instance_name, 'running')
|
149
|
+
await matcher.finish(f"Successfully started Lightsail instance: {instance_name}")
|
150
|
+
|
151
|
+
@lightsail_stop_matcher.handle()
|
152
|
+
@handle_non_finish_exceptions("An error occurred while stopping the Lightsail instance.")
|
153
|
+
async def handle_lightsail_stop(matcher: Matcher, args: Message = CommandArg()):
|
154
|
+
instance_name = args.extract_plain_text().strip()
|
155
|
+
if not instance_name:
|
156
|
+
await matcher.finish("Please provide a Lightsail instance name.")
|
157
|
+
|
158
|
+
await matcher.send(f"Sending stop command to {instance_name}...\nWaiting for it to become stopped...")
|
159
|
+
await lightsail_manager.stop_instance(instance_name)
|
160
|
+
await lightsail_manager.wait_for_status(instance_name, 'stopped')
|
161
|
+
await matcher.finish(f"Successfully stopped Lightsail instance: {instance_name}")
|
162
|
+
|
163
|
+
|
164
|
+
# --- Cost Explorer Handlers ---
|
165
|
+
|
166
|
+
@cost_matcher.handle()
|
167
|
+
@handle_non_finish_exceptions("An error occurred while fetching AWS cost data.")
|
168
|
+
async def handle_cost(matcher: Matcher, args: Message = CommandArg()):
|
169
|
+
sub_command = args.extract_plain_text().strip()
|
170
|
+
|
171
|
+
if sub_command == "today":
|
172
|
+
result = await cost_manager.get_cost_today()
|
173
|
+
cost = result['ResultsByTime'][0]['Total']['UnblendedCost']
|
174
|
+
await matcher.finish(f"AWS cost for today: {float(cost['Amount']):.4f} {cost['Unit']}")
|
175
|
+
elif sub_command == "month":
|
176
|
+
result = await cost_manager.get_cost_this_month()
|
177
|
+
cost = result['ResultsByTime'][0]['Total']['UnblendedCost']
|
178
|
+
await matcher.finish(f"AWS cost this month: {float(cost['Amount']):.4f} {cost['Unit']}")
|
179
|
+
elif sub_command == "month by_service":
|
180
|
+
result = await cost_manager.get_cost_this_month_by_service()
|
181
|
+
lines = ["Cost this month by service:"]
|
182
|
+
for group in sorted(result['ResultsByTime'][0]['Groups'], key=lambda x: float(x['Metrics']['UnblendedCost']['Amount']), reverse=True):
|
183
|
+
service_name = group['Keys'][0]
|
184
|
+
cost = group['Metrics']['UnblendedCost']
|
185
|
+
if float(cost['Amount']) > 0:
|
186
|
+
lines.append(f"- {service_name}: {float(cost['Amount']):.4f} {cost['Unit']}")
|
187
|
+
await matcher.finish("\n".join(lines))
|
188
|
+
else:
|
189
|
+
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.
|
4
|
-
Summary:
|
3
|
+
Version: 0.1.1
|
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
|
@@ -0,0 +1,8 @@
|
|
1
|
+
nonebot_plugin_awsmgmt/__init__.py,sha256=GVN4az13jO2EghO0OrqBA_qM_tkxLVHK5ZekqNuvN8s,8347
|
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.1.dist-info/METADATA,sha256=ts2Lx5PMnPj4udvIohSnfKiqyYIpmf-wpITZSPpan3Y,3684
|
7
|
+
nonebot_plugin_awsmgmt-0.1.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
8
|
+
nonebot_plugin_awsmgmt-0.1.1.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,,
|
File without changes
|