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 CHANGED
@@ -6,4 +6,5 @@ app = typer.Typer(
6
6
  pretty_exceptions_short=True,
7
7
  pretty_exceptions_show_locals=False,
8
8
  rich_markup_mode="rich",
9
+ no_args_is_help=True,
9
10
  )
@@ -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
@@ -107,7 +107,12 @@ def load_variables( # noqa: PLR0913
107
107
  console.print(table)
108
108
 
109
109
  # Retrieve the variables
110
- variables = _load_variables(map_arns_by_index, console=console, dry_run=dry_run)
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
- # TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
132
- # ; consider adding it if needed
133
- def _load_variables(map_arns: dict[str, _ARN], *, console: Console, dry_run: bool) -> dict[str, Any]:
134
- """Load the variables from the AWS Secrets Manager and SSM Parameter Store.
135
-
136
- Each secret or parameter should be a valid dictionary, where the keys are the variable names
137
- and the values are the variable values.
138
-
139
- The items are merged in the order of the key of provided mapping, overwriting the variables with the same name
140
- in the order of the keys.
141
- """
142
- console.print("🔍 Retrieving variables from AWS resources...")
143
- if dry_run:
144
- console.print("⚠️ Dry run mode enabled. Variables won't be loaded from AWS.")
145
-
146
- # Split the ARNs by resource types
147
- secrets_map, parameters_map = {}, {}
148
- for idx, arn in map_arns.items():
149
- if arn.startswith("arn:aws:secretsmanager:"):
150
- secrets_map[idx] = arn
151
- elif arn.startswith("arn:aws:ssm:"):
152
- parameters_map[idx] = arn
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
- msg = f"ARN of unsupported resource: {arn!r}"
155
- raise ValueError(msg)
185
+ secrets = self._retrieve_secrets(secrets_map)
186
+ parameters = self._retrieve_parameters(parameters_map)
156
187
 
157
- if secrets_map.keys() & parameters_map.keys():
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
- # Retrieve variables from AWS resources
162
- secrets: dict[str, _Variables]
163
- parameters: dict[str, _Variables]
164
- if dry_run:
165
- secrets = {idx: {} for idx, _ in secrets_map.items()}
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
- console.print(f"✅ Retrieved {len(secrets)} secrets and {len(parameters)} parameters.")
196
+ return merged_in_order
172
197
 
173
- # Merge the variables in order
174
- full_variables = secrets | parameters # Keys MUST NOT conflict
175
- merged_in_order = {}
176
- for _, variables in sorted(full_variables.items()):
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
- return merged_in_order
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
- # Type aliases for readability
183
- _ARN = str
184
- _Variables = dict[str, Any]
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
- def _retrieve_secrets(secrets_map: dict[str, _ARN]) -> dict[str, _Variables]:
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
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import aws_annoying.ecs_task_definition_lifecycle
5
5
  import aws_annoying.load_variables
6
+ import aws_annoying.mfa
6
7
  from aws_annoying.utils.debugger import input_as_args
7
8
 
8
9
  # App with all commands registered
@@ -0,0 +1,3 @@
1
+ from . import configure
2
+
3
+ __all__ = ("configure",)
@@ -0,0 +1,9 @@
1
+ import typer
2
+
3
+ from aws_annoying.app import app
4
+
5
+ mfa_app = typer.Typer(
6
+ no_args_is_help=True,
7
+ help="Commands to manage MFA authentication.",
8
+ )
9
+ app.add_typer(mfa_app, name="mfa")
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+ [![CI](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml/badge.svg)](https://github.com/lasuillard/aws-annoying/actions/workflows/ci.yaml)
32
+ [![codecov](https://codecov.io/gh/lasuillard/aws-annoying/graph/badge.svg?token=gbcHMVVz2k)](https://codecov.io/gh/lasuillard/aws-annoying)
33
+ ![GitHub Release](https://img.shields.io/github/v/release/lasuillard/aws-annoying)
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,,