aws-annoying 0.1.0__py3-none-any.whl → 0.2.0__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.
- aws_annoying/app.py +1 -0
- aws_annoying/ecs_task_definition_lifecycle.py +3 -0
- aws_annoying/load_variables.py +115 -105
- aws_annoying/main.py +1 -0
- aws_annoying/mfa/__init__.py +3 -0
- aws_annoying/mfa/_app.py +9 -0
- aws_annoying/mfa/configure.py +157 -0
- aws_annoying-0.2.0.dist-info/METADATA +35 -0
- aws_annoying-0.2.0.dist-info/RECORD +15 -0
- aws_annoying-0.1.0.dist-info/METADATA +0 -21
- aws_annoying-0.1.0.dist-info/RECORD +0 -12
- {aws_annoying-0.1.0.dist-info → aws_annoying-0.2.0.dist-info}/WHEEL +0 -0
- {aws_annoying-0.1.0.dist-info → aws_annoying-0.2.0.dist-info}/entry_points.txt +0 -0
- {aws_annoying-0.1.0.dist-info → aws_annoying-0.2.0.dist-info}/licenses/LICENSE +0 -0
aws_annoying/app.py
CHANGED
|
@@ -28,6 +28,9 @@ def ecs_task_definition_lifecycle(
|
|
|
28
28
|
),
|
|
29
29
|
) -> None:
|
|
30
30
|
"""Execute ECS task definition lifecycle."""
|
|
31
|
+
if dry_run:
|
|
32
|
+
print("⚠️ Dry run mode enabled. Will not perform any actual changes.")
|
|
33
|
+
|
|
31
34
|
ecs = boto3.client("ecs")
|
|
32
35
|
|
|
33
36
|
# Get all task definitions for the family
|
aws_annoying/load_variables.py
CHANGED
|
@@ -107,7 +107,12 @@ def load_variables( # noqa: PLR0913
|
|
|
107
107
|
console.print(table)
|
|
108
108
|
|
|
109
109
|
# Retrieve the variables
|
|
110
|
-
|
|
110
|
+
loader = VariableLoader(dry_run=dry_run, console=console)
|
|
111
|
+
try:
|
|
112
|
+
variables = loader.load(map_arns_by_index)
|
|
113
|
+
except Exception as exc: # noqa: BLE001
|
|
114
|
+
console.print(f"❌ Failed to load the variables: {exc!s}")
|
|
115
|
+
raise typer.Exit(1) from None
|
|
111
116
|
|
|
112
117
|
# Prepare the environment variables
|
|
113
118
|
env = os.environ.copy()
|
|
@@ -128,117 +133,122 @@ def load_variables( # noqa: PLR0913
|
|
|
128
133
|
raise typer.Exit(result.returncode)
|
|
129
134
|
|
|
130
135
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
136
|
+
# Type aliases for readability
|
|
137
|
+
_ARN = str
|
|
138
|
+
_Variables = dict[str, Any]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class VariableLoader: # noqa: D101
|
|
142
|
+
def __init__(self, *, console: Console | None = None, dry_run: bool) -> None:
|
|
143
|
+
"""Initialize the VariableLoader.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
dry_run: Whether to run in dry-run mode.
|
|
147
|
+
console: Rich console instance.
|
|
148
|
+
"""
|
|
149
|
+
self.console = console or Console(quiet=True)
|
|
150
|
+
self.dry_run = dry_run
|
|
151
|
+
|
|
152
|
+
# TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
|
|
153
|
+
# ; consider adding it if needed
|
|
154
|
+
def load(self, map_arns: dict[str, _ARN]) -> dict[str, Any]:
|
|
155
|
+
"""Load the variables from the AWS Secrets Manager and SSM Parameter Store.
|
|
156
|
+
|
|
157
|
+
Each secret or parameter should be a valid dictionary, where the keys are the variable names
|
|
158
|
+
and the values are the variable values.
|
|
159
|
+
|
|
160
|
+
The items are merged in the order of the key of provided mapping, overwriting the variables with the same name
|
|
161
|
+
in the order of the keys.
|
|
162
|
+
"""
|
|
163
|
+
self.console.print("🔍 Retrieving variables from AWS resources...")
|
|
164
|
+
if self.dry_run:
|
|
165
|
+
self.console.print("⚠️ Dry run mode enabled. Variables won't be loaded from AWS.")
|
|
166
|
+
|
|
167
|
+
# Split the ARNs by resource types
|
|
168
|
+
secrets_map, parameters_map = {}, {}
|
|
169
|
+
for idx, arn in map_arns.items():
|
|
170
|
+
if arn.startswith("arn:aws:secretsmanager:"):
|
|
171
|
+
secrets_map[idx] = arn
|
|
172
|
+
elif arn.startswith("arn:aws:ssm:"):
|
|
173
|
+
parameters_map[idx] = arn
|
|
174
|
+
else:
|
|
175
|
+
msg = f"Unsupported resource: {arn!r}"
|
|
176
|
+
raise ValueError(msg)
|
|
177
|
+
|
|
178
|
+
# Retrieve variables from AWS resources
|
|
179
|
+
secrets: dict[str, _Variables]
|
|
180
|
+
parameters: dict[str, _Variables]
|
|
181
|
+
if self.dry_run:
|
|
182
|
+
secrets = {idx: {} for idx, _ in secrets_map.items()}
|
|
183
|
+
parameters = {idx: {} for idx, _ in parameters_map.items()}
|
|
153
184
|
else:
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
secrets = self._retrieve_secrets(secrets_map)
|
|
186
|
+
parameters = self._retrieve_parameters(parameters_map)
|
|
156
187
|
|
|
157
|
-
|
|
158
|
-
msg = "Keys in secrets and parameters MUST NOT conflict."
|
|
159
|
-
raise ValueError(msg)
|
|
188
|
+
self.console.print(f"✅ Retrieved {len(secrets)} secrets and {len(parameters)} parameters.")
|
|
160
189
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
parameters = {idx: {} for idx, _ in parameters_map.items()}
|
|
167
|
-
else:
|
|
168
|
-
secrets = _retrieve_secrets(secrets_map)
|
|
169
|
-
parameters = _retrieve_parameters(parameters_map)
|
|
190
|
+
# Merge the variables in order
|
|
191
|
+
full_variables = secrets | parameters # Keys MUST NOT conflict
|
|
192
|
+
merged_in_order = {}
|
|
193
|
+
for _, variables in sorted(full_variables.items()):
|
|
194
|
+
merged_in_order.update(variables)
|
|
170
195
|
|
|
171
|
-
|
|
196
|
+
return merged_in_order
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
merged_in_order.update(variables)
|
|
198
|
+
def _retrieve_secrets(self, secrets_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
199
|
+
"""Retrieve the secrets from AWS Secrets Manager."""
|
|
200
|
+
if not secrets_map:
|
|
201
|
+
return {}
|
|
178
202
|
|
|
179
|
-
|
|
203
|
+
secretsmanager = boto3.client("secretsmanager")
|
|
180
204
|
|
|
205
|
+
# Retrieve the secrets
|
|
206
|
+
arns = list(secrets_map.values())
|
|
207
|
+
response = secretsmanager.batch_get_secret_value(SecretIdList=arns)
|
|
208
|
+
if errors := response["Errors"]:
|
|
209
|
+
msg = f"Failed to retrieve secrets: {errors!r}"
|
|
210
|
+
raise ValueError(msg)
|
|
181
211
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
# Parse the secrets
|
|
213
|
+
secrets = response["SecretValues"]
|
|
214
|
+
result = {}
|
|
215
|
+
for secret in secrets:
|
|
216
|
+
arn = secret["ARN"]
|
|
217
|
+
order_key = next(key for key, value in secrets_map.items() if value == arn)
|
|
218
|
+
data = json.loads(secret["SecretString"])
|
|
219
|
+
if not isinstance(data, dict):
|
|
220
|
+
msg = f"Secret data must be a valid dictionary, but got: {type(data)!r}"
|
|
221
|
+
raise TypeError(msg)
|
|
222
|
+
|
|
223
|
+
result[order_key] = data
|
|
224
|
+
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
def _retrieve_parameters(self, parameters_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
228
|
+
"""Retrieve the parameters from AWS SSM Parameter Store."""
|
|
229
|
+
if not parameters_map:
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
ssm = boto3.client("ssm")
|
|
233
|
+
|
|
234
|
+
# Retrieve the parameters
|
|
235
|
+
parameter_names = list(parameters_map.values())
|
|
236
|
+
response = ssm.get_parameters(Names=parameter_names, WithDecryption=True)
|
|
237
|
+
if errors := response["InvalidParameters"]:
|
|
238
|
+
msg = f"Failed to retrieve parameters: {errors!r}"
|
|
239
|
+
raise ValueError(msg)
|
|
240
|
+
|
|
241
|
+
# Parse the parameters
|
|
242
|
+
parameters = response["Parameters"]
|
|
243
|
+
result = {}
|
|
244
|
+
for parameter in parameters:
|
|
245
|
+
arn = parameter["ARN"]
|
|
246
|
+
order_key = next(key for key, value in parameters_map.items() if value == arn)
|
|
247
|
+
data = json.loads(parameter["Value"])
|
|
248
|
+
if not isinstance(data, dict):
|
|
249
|
+
msg = f"Parameter data must be a valid dictionary, but got: {type(data)!r}"
|
|
250
|
+
raise TypeError(msg)
|
|
185
251
|
|
|
252
|
+
result[order_key] = data
|
|
186
253
|
|
|
187
|
-
|
|
188
|
-
"""Retrieve the secrets from AWS Secrets Manager."""
|
|
189
|
-
if not secrets_map:
|
|
190
|
-
return {}
|
|
191
|
-
|
|
192
|
-
secretsmanager = boto3.client("secretsmanager")
|
|
193
|
-
|
|
194
|
-
# Retrieve the secrets
|
|
195
|
-
arns = list(secrets_map.values())
|
|
196
|
-
response = secretsmanager.batch_get_secret_value(SecretIdList=arns)
|
|
197
|
-
if errors := response["Errors"]:
|
|
198
|
-
msg = f"Failed to retrieve secrets: {errors!r}"
|
|
199
|
-
raise ValueError(msg)
|
|
200
|
-
|
|
201
|
-
# Parse the secrets
|
|
202
|
-
secrets = response["SecretValues"]
|
|
203
|
-
result = {}
|
|
204
|
-
for secret in secrets:
|
|
205
|
-
arn = secret["ARN"]
|
|
206
|
-
order_key = next(key for key, value in secrets_map.items() if value == arn)
|
|
207
|
-
data = json.loads(secret["SecretString"])
|
|
208
|
-
if not isinstance(data, dict):
|
|
209
|
-
msg = f"Secret data must be a valid dictionary, but got: {type(data)!r}"
|
|
210
|
-
raise TypeError(msg)
|
|
211
|
-
|
|
212
|
-
result[order_key] = data
|
|
213
|
-
|
|
214
|
-
return result
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _retrieve_parameters(parameters_map: dict[str, _ARN]) -> dict[str, _Variables]:
|
|
218
|
-
"""Retrieve the parameters from AWS SSM Parameter Store."""
|
|
219
|
-
if not parameters_map:
|
|
220
|
-
return {}
|
|
221
|
-
|
|
222
|
-
ssm = boto3.client("ssm")
|
|
223
|
-
|
|
224
|
-
# Retrieve the parameters
|
|
225
|
-
parameter_names = list(parameters_map.values())
|
|
226
|
-
response = ssm.get_parameters(Names=parameter_names, WithDecryption=True)
|
|
227
|
-
if errors := response["InvalidParameters"]:
|
|
228
|
-
msg = f"Failed to retrieve parameters: {errors!r}"
|
|
229
|
-
raise ValueError(msg)
|
|
230
|
-
|
|
231
|
-
# Parse the parameters
|
|
232
|
-
parameters = response["Parameters"]
|
|
233
|
-
result = {}
|
|
234
|
-
for parameter in parameters:
|
|
235
|
-
arn = parameter["ARN"]
|
|
236
|
-
order_key = next(key for key, value in parameters_map.items() if value == arn)
|
|
237
|
-
data = json.loads(parameter["Value"])
|
|
238
|
-
if not isinstance(data, dict):
|
|
239
|
-
msg = f"Parameter data must be a valid dictionary, but got: {type(data)!r}"
|
|
240
|
-
raise TypeError(msg)
|
|
241
|
-
|
|
242
|
-
result[order_key] = data
|
|
243
|
-
|
|
244
|
-
return result
|
|
254
|
+
return result
|
aws_annoying/main.py
CHANGED
aws_annoying/mfa/_app.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
from pathlib import Path # noqa: TC003
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
import typer
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
from rich import print # noqa: A004
|
|
11
|
+
from rich.prompt import Prompt
|
|
12
|
+
|
|
13
|
+
from ._app import mfa_app
|
|
14
|
+
|
|
15
|
+
_CONFIG_INI_SECTION = "aws-annoying:mfa"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@mfa_app.command()
|
|
19
|
+
def configure( # noqa: PLR0913
|
|
20
|
+
*,
|
|
21
|
+
mfa_profile: Optional[str] = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
help="The MFA profile to configure.",
|
|
24
|
+
),
|
|
25
|
+
mfa_source_profile: Optional[str] = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
help="The AWS profile to use to retrieve MFA credentials.",
|
|
28
|
+
),
|
|
29
|
+
mfa_serial_number: Optional[str] = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
help="The MFA device serial number. It is required if not persisted in configuration.",
|
|
32
|
+
show_default=False,
|
|
33
|
+
),
|
|
34
|
+
mfa_token_code: Optional[str] = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
help="The MFA token code.",
|
|
37
|
+
show_default=False,
|
|
38
|
+
),
|
|
39
|
+
aws_credentials: Path = typer.Option( # noqa: B008
|
|
40
|
+
"~/.aws/credentials",
|
|
41
|
+
help="The path to the AWS credentials file.",
|
|
42
|
+
),
|
|
43
|
+
aws_config: Path = typer.Option( # noqa: B008
|
|
44
|
+
"~/.aws/config",
|
|
45
|
+
help="The path to the AWS config file. Used to persist the MFA configuration.",
|
|
46
|
+
),
|
|
47
|
+
persist: bool = typer.Option(
|
|
48
|
+
True, # noqa: FBT003
|
|
49
|
+
help="Persist the MFA configuration.",
|
|
50
|
+
),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Configure AWS profile for MFA."""
|
|
53
|
+
# Expand user home directory
|
|
54
|
+
aws_credentials = aws_credentials.expanduser()
|
|
55
|
+
aws_config = aws_config.expanduser()
|
|
56
|
+
|
|
57
|
+
# Load configuration
|
|
58
|
+
mfa_config, exists = _MfaConfig.from_ini_file(aws_config, _CONFIG_INI_SECTION)
|
|
59
|
+
if exists:
|
|
60
|
+
print(f"⚙️ Loaded MFA configuration from AWS config ({aws_config}).")
|
|
61
|
+
|
|
62
|
+
mfa_profile = (
|
|
63
|
+
mfa_profile
|
|
64
|
+
or mfa_config.mfa_profile
|
|
65
|
+
# _
|
|
66
|
+
or Prompt.ask("👤 Enter name of MFA profile to configure", default="mfa")
|
|
67
|
+
)
|
|
68
|
+
mfa_source_profile = (
|
|
69
|
+
mfa_source_profile
|
|
70
|
+
or mfa_config.mfa_source_profile
|
|
71
|
+
or Prompt.ask("👤 Enter AWS profile to use to retrieve MFA credentials", default="default")
|
|
72
|
+
)
|
|
73
|
+
mfa_serial_number = (
|
|
74
|
+
mfa_serial_number
|
|
75
|
+
or mfa_config.mfa_serial_number
|
|
76
|
+
# _
|
|
77
|
+
or Prompt.ask("🔒 Enter MFA serial number")
|
|
78
|
+
)
|
|
79
|
+
mfa_token_code = (
|
|
80
|
+
mfa_token_code
|
|
81
|
+
# _
|
|
82
|
+
or Prompt.ask("🔑 Enter MFA token code")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get credentials
|
|
86
|
+
print(f"💬 Retrieving MFA credentials using profile [bold]{mfa_source_profile}[/bold]")
|
|
87
|
+
session = boto3.session.Session(profile_name=mfa_source_profile)
|
|
88
|
+
sts = session.client("sts")
|
|
89
|
+
response = sts.get_session_token(
|
|
90
|
+
SerialNumber=mfa_serial_number,
|
|
91
|
+
TokenCode=mfa_token_code,
|
|
92
|
+
)
|
|
93
|
+
credentials = response["Credentials"]
|
|
94
|
+
|
|
95
|
+
# Update MFA profile in AWS credentials
|
|
96
|
+
print(f"✅ Updating MFA profile ([bold]{mfa_profile}[/bold]) to AWS credentials ({aws_credentials})")
|
|
97
|
+
_update_credentials(
|
|
98
|
+
aws_credentials,
|
|
99
|
+
mfa_profile, # type: ignore[arg-type]
|
|
100
|
+
access_key=credentials["AccessKeyId"],
|
|
101
|
+
secret_key=credentials["SecretAccessKey"],
|
|
102
|
+
session_token=credentials["SessionToken"],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Persist MFA configuration
|
|
106
|
+
if persist:
|
|
107
|
+
print(
|
|
108
|
+
f"✅ Persisting MFA configuration in AWS config ({aws_config}),"
|
|
109
|
+
f" in [bold]{_CONFIG_INI_SECTION}[/bold] section.",
|
|
110
|
+
)
|
|
111
|
+
mfa_config.mfa_profile = mfa_profile
|
|
112
|
+
mfa_config.mfa_source_profile = mfa_source_profile
|
|
113
|
+
mfa_config.mfa_serial_number = mfa_serial_number
|
|
114
|
+
mfa_config.save_ini_file(aws_config, _CONFIG_INI_SECTION)
|
|
115
|
+
else:
|
|
116
|
+
print("⚠️ MFA configuration not persisted.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _MfaConfig(BaseModel):
|
|
120
|
+
model_config = ConfigDict(extra="ignore")
|
|
121
|
+
|
|
122
|
+
mfa_profile: Optional[str] = None
|
|
123
|
+
mfa_source_profile: Optional[str] = None
|
|
124
|
+
mfa_serial_number: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
def save_ini_file(self, path: Path, section_key: str) -> None:
|
|
127
|
+
"""Save configuration to an AWS config file."""
|
|
128
|
+
config_ini = configparser.ConfigParser()
|
|
129
|
+
config_ini.read(path)
|
|
130
|
+
config_ini.setdefault(section_key, {})
|
|
131
|
+
for k, v in self.model_dump(exclude_none=True).items():
|
|
132
|
+
config_ini[section_key][k] = v
|
|
133
|
+
|
|
134
|
+
with path.open("w") as f:
|
|
135
|
+
config_ini.write(f)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_ini_file(cls, path: Path, section_key: str) -> tuple[_MfaConfig, bool]:
|
|
139
|
+
"""Load configuration from an AWS config file, with boolean indicating if the config already exists."""
|
|
140
|
+
config_ini = configparser.ConfigParser()
|
|
141
|
+
config_ini.read(path)
|
|
142
|
+
if config_ini.has_section(section_key):
|
|
143
|
+
section = dict(config_ini.items(section_key))
|
|
144
|
+
return cls.model_validate(section), True
|
|
145
|
+
|
|
146
|
+
return cls(), False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _update_credentials(path: Path, profile: str, *, access_key: str, secret_key: str, session_token: str) -> None:
|
|
150
|
+
credentials_ini = configparser.ConfigParser()
|
|
151
|
+
credentials_ini.read(path)
|
|
152
|
+
credentials_ini.setdefault(profile, {})
|
|
153
|
+
credentials_ini[profile]["aws_access_key_id"] = access_key
|
|
154
|
+
credentials_ini[profile]["aws_secret_access_key"] = secret_key
|
|
155
|
+
credentials_ini[profile]["aws_session_token"] = session_token
|
|
156
|
+
with path.open("w") as f:
|
|
157
|
+
credentials_ini.write(f)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aws-annoying
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Utils to handle some annoying AWS tasks.
|
|
5
|
+
Author-email: Yuchan Lee <lasuillard@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: <4.0,>=3.9
|
|
9
|
+
Requires-Dist: boto3>=1.37.1
|
|
10
|
+
Requires-Dist: pydantic>=2.10.6
|
|
11
|
+
Requires-Dist: typer>=0.15.1
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: boto3-stubs[ecs,secretsmanager,ssm,sts]>=1.37.1; extra == 'dev'
|
|
14
|
+
Requires-Dist: mypy~=1.15.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff~=0.9.9; extra == 'dev'
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: coverage~=7.6.0; extra == 'test'
|
|
18
|
+
Requires-Dist: moto[ecs,secretsmanager,server,ssm]~=5.1.1; extra == 'test'
|
|
19
|
+
Requires-Dist: pytest-cov~=6.0.0; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest-env~=1.1.1; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest-snapshot>=0.9.0; extra == 'test'
|
|
22
|
+
Requires-Dist: pytest-sugar~=1.0.0; extra == 'test'
|
|
23
|
+
Requires-Dist: pytest-xdist>=3.6.1; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest~=8.3.2; extra == 'test'
|
|
25
|
+
Requires-Dist: testcontainers[localstack]>=4.9.2; extra == 'test'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# aws-annoying
|
|
29
|
+
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml)
|
|
32
|
+
[](https://codecov.io/gh/lasuillard/aws-annoying)
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Utils to handle some annoying AWS tasks.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aws_annoying/app.py,sha256=sp50uVoAl4D6Wk3DFpzKZzSsxmSxNYejFxm62b_Kxps,201
|
|
3
|
+
aws_annoying/ecs_task_definition_lifecycle.py,sha256=hiypU5RD5moo0KYHn8YRAyGDSMKh-zPnfxfFYWm6w78,1744
|
|
4
|
+
aws_annoying/load_variables.py,sha256=380xT1i85HWybgOIWn72xGCDgqYJ2OSa9VOKMlyHg8M,9488
|
|
5
|
+
aws_annoying/main.py,sha256=jngo15w50Jf6nr63N-yV6AEYsFKYzObJc0wI364zS0s,451
|
|
6
|
+
aws_annoying/mfa/__init__.py,sha256=rbEGhw5lOQZV_XAc3nSbo56JVhsSPpeOgEtiAy9qzEA,50
|
|
7
|
+
aws_annoying/mfa/_app.py,sha256=hpa1Bfx8lRsuZfujM-RyaYU5llOuwGKgf4FwEydthU0,185
|
|
8
|
+
aws_annoying/mfa/configure.py,sha256=i5e0qZBFYafuv3D59eP_JXOMrWSRW_bZp0xgIpOlaPE,5364
|
|
9
|
+
aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
|
|
11
|
+
aws_annoying-0.2.0.dist-info/METADATA,sha256=0neXI2tmvEBoLQ30VMWfbssan-ZvboVlEpW305veurk,1607
|
|
12
|
+
aws_annoying-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
aws_annoying-0.2.0.dist-info/entry_points.txt,sha256=s0l5LK26zEUxLRkBHH9Bu5aH1bOPvYVoTMnUcFlzjm8,62
|
|
14
|
+
aws_annoying-0.2.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
|
|
15
|
+
aws_annoying-0.2.0.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: aws-annoying
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Utils to handle some annoying AWS tasks.
|
|
5
|
-
Author-email: Yuchan Lee <lasuillard@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: <4.0,>=3.9
|
|
9
|
-
Requires-Dist: boto3>=1.37.1
|
|
10
|
-
Requires-Dist: typer>=0.15.1
|
|
11
|
-
Provides-Extra: dev
|
|
12
|
-
Requires-Dist: boto3-stubs[ecs,secretsmanager,ssm]>=1.37.1; extra == 'dev'
|
|
13
|
-
Requires-Dist: mypy~=1.15.0; extra == 'dev'
|
|
14
|
-
Requires-Dist: ruff~=0.9.9; extra == 'dev'
|
|
15
|
-
Provides-Extra: test
|
|
16
|
-
Requires-Dist: coverage~=7.6.0; extra == 'test'
|
|
17
|
-
Requires-Dist: moto[ecs,secretsmanager,server,ssm]; extra == 'test'
|
|
18
|
-
Requires-Dist: pytest-cov~=6.0.0; extra == 'test'
|
|
19
|
-
Requires-Dist: pytest-env~=1.1.1; extra == 'test'
|
|
20
|
-
Requires-Dist: pytest-sugar~=1.0.0; extra == 'test'
|
|
21
|
-
Requires-Dist: pytest~=8.3.2; extra == 'test'
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
aws_annoying/app.py,sha256=JyUl5f4FkYB2r6l5k0tKQUOJUa-d-hsIKOCMtdmvCOo,175
|
|
3
|
-
aws_annoying/ecs_task_definition_lifecycle.py,sha256=KTAkaEbRdFXTti6l36f78aVGWs1soVp8eqV2Ft7b-hc,1644
|
|
4
|
-
aws_annoying/load_variables.py,sha256=J2lO7ARQ9Dwxrbv8Rr2WUQe5A6bPM1pgEycMjWKEif8,8745
|
|
5
|
-
aws_annoying/main.py,sha256=BQIWH7hEvT3MSTPwfljwRvk9sVZOewagrbtW9lEtZhQ,427
|
|
6
|
-
aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
|
|
8
|
-
aws_annoying-0.1.0.dist-info/METADATA,sha256=CEEyikRPVswiVzI9H5D_bWwqgRobilGCQgf6dPxZT94,803
|
|
9
|
-
aws_annoying-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
-
aws_annoying-0.1.0.dist-info/entry_points.txt,sha256=s0l5LK26zEUxLRkBHH9Bu5aH1bOPvYVoTMnUcFlzjm8,62
|
|
11
|
-
aws_annoying-0.1.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
|
|
12
|
-
aws_annoying-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|