aws-annoying 0.2.1__py3-none-any.whl → 0.4.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.
Files changed (30) hide show
  1. aws_annoying/cli/__init__.py +0 -0
  2. aws_annoying/{ecs_task_definition_lifecycle.py → cli/ecs_task_definition_lifecycle.py} +27 -0
  3. aws_annoying/cli/load_variables.py +139 -0
  4. aws_annoying/{main.py → cli/main.py} +4 -3
  5. aws_annoying/{mfa → cli/mfa}/_app.py +1 -1
  6. aws_annoying/{mfa → cli/mfa}/configure.py +4 -45
  7. aws_annoying/cli/session_manager/__init__.py +3 -0
  8. aws_annoying/cli/session_manager/_app.py +9 -0
  9. aws_annoying/cli/session_manager/_common.py +24 -0
  10. aws_annoying/cli/session_manager/install.py +39 -0
  11. aws_annoying/cli/session_manager/port_forward.py +126 -0
  12. aws_annoying/cli/session_manager/start.py +9 -0
  13. aws_annoying/cli/session_manager/stop.py +50 -0
  14. aws_annoying/mfa.py +54 -0
  15. aws_annoying/session_manager/__init__.py +4 -0
  16. aws_annoying/session_manager/errors.py +10 -0
  17. aws_annoying/session_manager/session_manager.py +318 -0
  18. aws_annoying/utils/downloader.py +58 -0
  19. aws_annoying/utils/platform.py +27 -0
  20. aws_annoying/variables.py +133 -0
  21. {aws_annoying-0.2.1.dist-info → aws_annoying-0.4.0.dist-info}/METADATA +8 -5
  22. aws_annoying-0.4.0.dist-info/RECORD +30 -0
  23. aws_annoying-0.4.0.dist-info/entry_points.txt +2 -0
  24. aws_annoying/load_variables.py +0 -254
  25. aws_annoying-0.2.1.dist-info/RECORD +0 -15
  26. aws_annoying-0.2.1.dist-info/entry_points.txt +0 -2
  27. /aws_annoying/{app.py → cli/app.py} +0 -0
  28. /aws_annoying/{mfa → cli/mfa}/__init__.py +0 -0
  29. {aws_annoying-0.2.1.dist-info → aws_annoying-0.4.0.dist-info}/WHEEL +0 -0
  30. {aws_annoying-0.2.1.dist-info → aws_annoying-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,318 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import platform
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any, NamedTuple
13
+
14
+ import boto3
15
+
16
+ from aws_annoying.utils.platform import command_as_root, is_root, os_release
17
+
18
+ from .errors import PluginNotInstalledError, UnsupportedPlatformError
19
+
20
+ if TYPE_CHECKING:
21
+ from aws_annoying.utils.downloader import AbstractDownloader
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # TODO(lasuillard): Platform checking is spread everywhere, should be moved to a single place
26
+
27
+
28
+ class SessionManager:
29
+ """AWS Session Manager plugin manager."""
30
+
31
+ def __init__(self, *, session: boto3.session.Session | None = None, downloader: AbstractDownloader) -> None:
32
+ """Initialize SessionManager.
33
+
34
+ Args:
35
+ session: Boto3 session to use for AWS operations.
36
+ downloader: File downloader to use for downloading the plugin.
37
+ """
38
+ self.session = session or boto3.session.Session()
39
+ self.downloader = downloader
40
+
41
+ # ------------------------------------------------------------------------
42
+ # Installation
43
+ # ------------------------------------------------------------------------
44
+ def verify_installation(self) -> tuple[bool, Path | None, str | None]:
45
+ """Verify installation of AWS Session Manager plugin.
46
+
47
+ Returns:
48
+ 3-tuple of boolean flag indicating whether plugin installed, binary path and version string.
49
+ """
50
+ # Find plugin binary
51
+ if not (binary_path := self._get_binary_path()):
52
+ return False, None, None
53
+
54
+ # Check version
55
+ result_bytes = subprocess.run( # noqa: S603
56
+ [str(binary_path), "--version"],
57
+ check=True,
58
+ capture_output=True,
59
+ )
60
+ result = result_bytes.stdout.decode().strip()
61
+ if not bool(re.match(r"[\d\.]+", result)):
62
+ return False, binary_path, result
63
+
64
+ return True, binary_path, result
65
+
66
+ def _get_binary_path(self) -> Path | None:
67
+ """Get the path to the session-manager-plugin binary."""
68
+ binary_path_str = shutil.which("session-manager-plugin")
69
+ if not binary_path_str:
70
+ if platform.system() == "Windows":
71
+ # Windows: use the default installation path
72
+ binary_path = (
73
+ Path(os.environ["ProgramFiles"]) # noqa: SIM112
74
+ / "Amazon"
75
+ / "SessionManagerPlugin"
76
+ / "bin"
77
+ / "session-manager-plugin.exe"
78
+ )
79
+ if binary_path.is_file():
80
+ return binary_path.absolute()
81
+
82
+ return None
83
+
84
+ return Path(binary_path_str).absolute()
85
+
86
+ def install(
87
+ self,
88
+ *,
89
+ os: str | None = None,
90
+ linux_distribution: _LinuxDistribution | None = None,
91
+ arch: str | None = None,
92
+ root: bool | None = None,
93
+ ) -> None:
94
+ """Install AWS Session Manager plugin.
95
+
96
+ Args:
97
+ os: The operating system to install the plugin on. If `None`, will use the current operating system.
98
+ linux_distribution: The Linux distribution to install the plugin on.
99
+ If `None` and current `os` is `"Linux"`, will try to detect the distribution from current system.
100
+ arch: The architecture to install the plugin on. If `None`, will use the current architecture.
101
+ root: Whether to run the installation as root. If `None`, will check if the current user is root.
102
+ """
103
+ os = os or platform.system()
104
+ arch = arch or platform.machine()
105
+
106
+ if os == "Windows":
107
+ self._install_windows()
108
+ elif os == "Darwin":
109
+ self._install_macos(arch=arch, root=root or is_root())
110
+ elif os == "Linux":
111
+ linux_distribution = linux_distribution or _detect_linux_distribution()
112
+ self._install_linux(linux_distribution=linux_distribution, arch=arch, root=root or is_root())
113
+ else:
114
+ msg = f"Unsupported operating system: {os}"
115
+ raise UnsupportedPlatformError(msg)
116
+
117
+ def before_install(self, command: list[str]) -> None:
118
+ """Hook to run before invoking plugin installation command."""
119
+
120
+ # https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-windows.html
121
+ def _install_windows(self) -> None:
122
+ """Install session-manager-plugin on Windows via EXE installer."""
123
+ download_url = (
124
+ "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/windows/SessionManagerPluginSetup.exe"
125
+ )
126
+ with tempfile.TemporaryDirectory() as temp_dir:
127
+ p = Path(temp_dir)
128
+ exe_installer = self.downloader.download(download_url, to=p / "SessionManagerPluginSetup.exe")
129
+ command = [str(exe_installer), "/quiet"]
130
+ self.before_install(command)
131
+ subprocess.call(command, cwd=p) # noqa: S603
132
+
133
+ # https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-macos-overview.html
134
+ def _install_macos(self, *, arch: str, root: bool) -> None:
135
+ """Install session-manager-plugin on macOS via signed installer."""
136
+ # ! Intel chip will not be supported
137
+ if arch == "x86_64":
138
+ download_url = (
139
+ "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac/session-manager-plugin.pkg"
140
+ )
141
+ elif arch == "arm64":
142
+ download_url = (
143
+ "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac_arm64/session-manager-plugin.pkg"
144
+ )
145
+ else:
146
+ msg = f"Architecture {arch} not supported for macOS"
147
+ raise UnsupportedPlatformError(msg)
148
+
149
+ with tempfile.TemporaryDirectory() as temp_dir:
150
+ p = Path(temp_dir)
151
+ pkg_installer = self.downloader.download(download_url, to=p / "session-manager-plugin.pkg")
152
+
153
+ # Run installer
154
+ command = command_as_root(
155
+ ["installer", "-pkg", str(pkg_installer), "-target", "/"],
156
+ root=root,
157
+ )
158
+ self.before_install(command)
159
+ subprocess.call(command, cwd=p) # noqa: S603
160
+
161
+ # Symlink
162
+ command = [
163
+ "ln",
164
+ "-s",
165
+ "/usr/local/sessionmanagerplugin/bin/session-manager-plugin",
166
+ "/usr/local/bin/session-manager-plugin",
167
+ ]
168
+ if not root:
169
+ command = ["sudo", *command]
170
+
171
+ logger.info("Running %s to create symlink", " ".join(command))
172
+ Path("/usr/local/bin").mkdir(exist_ok=True)
173
+ subprocess.call(command, cwd=p) # noqa: S603
174
+
175
+ # https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-linux-overview.html
176
+ def _install_linux(
177
+ self,
178
+ *,
179
+ linux_distribution: _LinuxDistribution,
180
+ arch: str,
181
+ root: bool,
182
+ ) -> None:
183
+ name = linux_distribution.name
184
+ version = linux_distribution.version
185
+
186
+ # Debian / Ubuntu
187
+ if name in ("debian", "ubuntu"):
188
+ logger.info("Detected Linux distribution: Debian / Ubuntu")
189
+
190
+ # Download installer
191
+ url_map = {
192
+ "x86_64": "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb",
193
+ "x86": "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_32bit/session-manager-plugin.deb",
194
+ "arm64": "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_arm64/session-manager-plugin.deb",
195
+ }
196
+ download_url = url_map.get(arch)
197
+ if not download_url:
198
+ msg = f"Architecture {arch} not supported for distribution {name}"
199
+ raise UnsupportedPlatformError(msg)
200
+
201
+ with tempfile.TemporaryDirectory() as temp_dir:
202
+ p = Path(temp_dir)
203
+ deb_installer = self.downloader.download(download_url, to=p / "session-manager-plugin.deb")
204
+
205
+ # Invoke installation command
206
+ command = command_as_root(["dpkg", "--install", str(deb_installer)], root=root)
207
+ self.before_install(command)
208
+ subprocess.call(command, cwd=p) # noqa: S603
209
+
210
+ # Amazon Linux / RHEL
211
+ elif name in ("amzn", "rhel"):
212
+ logger.info("Detected Linux distribution: Amazon Linux / RHEL")
213
+
214
+ # Determine package URL and package manager
215
+ url_map = {
216
+ "x86_64": "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin.rpm",
217
+ "x86": "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_32bit/session-manager-plugin.rpm",
218
+ "arm64": "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_arm64/session-manager-plugin.rpm",
219
+ }
220
+ package_url = url_map.get(arch)
221
+ if not package_url:
222
+ msg = f"Architecture {arch} not supported for distribution {name}"
223
+ raise UnsupportedPlatformError(msg)
224
+
225
+ use_yum = (
226
+ # Amazon Linux 2
227
+ name == "amzn" and version.startswith("2")
228
+ ) or (
229
+ # RHEL 7 Maipo (8 Ootpa / 9 Plow)
230
+ name == "rhel" and "Maipo" in version
231
+ ) # ... else use dnf
232
+ package_manager = "yum" if use_yum else "dnf"
233
+
234
+ # Invoke installation command
235
+ command = command_as_root([package_manager, "install", "-y", package_url], root=root)
236
+ self.before_install(command)
237
+ subprocess.call(command) # noqa: S603
238
+
239
+ else:
240
+ msg = f"Unsupported distribution: {name}"
241
+ raise UnsupportedPlatformError(msg)
242
+
243
+ # ------------------------------------------------------------------------
244
+ # Session
245
+ # ------------------------------------------------------------------------
246
+ def start(
247
+ self,
248
+ *,
249
+ target: str,
250
+ document_name: str,
251
+ parameters: dict[str, Any],
252
+ reason: str | None = None,
253
+ log_file: Path | None = None,
254
+ ) -> subprocess.Popen:
255
+ """Start new session.
256
+
257
+ Args:
258
+ target: The target instance ID or name.
259
+ document_name: The SSM document name to use for the session.
260
+ parameters: The parameters to pass to the SSM document.
261
+ reason: The reason for starting the session.
262
+ log_file: Optional file to log output to.
263
+
264
+ Returns:
265
+ Process ID of the session.
266
+ """
267
+ is_installed, binary_path, version = self.verify_installation()
268
+ if not is_installed:
269
+ msg = "Session Manager plugin is not installed."
270
+ raise PluginNotInstalledError(msg)
271
+
272
+ ssm = self.session.client("ssm")
273
+ response = ssm.start_session(
274
+ Target=target,
275
+ DocumentName=document_name,
276
+ Parameters=parameters,
277
+ # ? Reason is optional but it doesn't allow empty string or `None`
278
+ **({"Reason": reason} if reason else {}),
279
+ )
280
+
281
+ region = self.session.region_name
282
+ command = [
283
+ str(binary_path),
284
+ json.dumps(response),
285
+ region,
286
+ "StartSession",
287
+ self.session.profile_name,
288
+ json.dumps({"Target": target}),
289
+ f"https://ssm.{region}.amazonaws.com",
290
+ ]
291
+
292
+ stdout: subprocess._FILE
293
+ if log_file is not None: # noqa: SIM108
294
+ stdout = log_file.open(mode="at+", buffering=1)
295
+ else:
296
+ stdout = subprocess.DEVNULL
297
+
298
+ return subprocess.Popen( # noqa: S603
299
+ command,
300
+ stdout=stdout,
301
+ stderr=subprocess.STDOUT,
302
+ text=True,
303
+ close_fds=False, # FD inherited from parent process
304
+ )
305
+
306
+
307
+ # ? Could be moved to utils, but didn't because it's too specific to this module
308
+ class _LinuxDistribution(NamedTuple):
309
+ name: str
310
+ version: str
311
+
312
+
313
+ def _detect_linux_distribution() -> _LinuxDistribution:
314
+ """Autodetect current Linux distribution."""
315
+ osr = os_release()
316
+ name = osr.get("ID", "").lower()
317
+ version = osr.get("VERSION", "")
318
+ return _LinuxDistribution(name=name, version=version)
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from abc import ABC, abstractmethod
5
+ from typing import TYPE_CHECKING
6
+
7
+ import requests
8
+ from tqdm import tqdm
9
+
10
+ if TYPE_CHECKING:
11
+ from pathlib import Path
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class AbstractDownloader(ABC):
17
+ """Abstract downloader class for downloading files."""
18
+
19
+ @abstractmethod
20
+ def download(self, url: str, *, to: Path) -> Path:
21
+ """Download file from URL to path."""
22
+
23
+
24
+ class DummyDownloader(AbstractDownloader):
25
+ """Dummy downloader that does nothing (mainly for testing purposes)."""
26
+
27
+ def download(self, url: str, *, to: Path) -> Path:
28
+ """Download file from URL to path."""
29
+ logger.debug("Dummy downloader called for URL (%s) to %s.", url, to)
30
+ return to.absolute()
31
+
32
+
33
+ class TQDMDownloader(AbstractDownloader):
34
+ """Downloader with TQDM progress bar."""
35
+
36
+ def download(self, url: str, *, to: Path) -> Path:
37
+ """Download file from URL to path."""
38
+ # https://gist.github.com/yanqd0/c13ed29e29432e3cf3e7c38467f42f51
39
+ logger.info("Downloading file from URL (%s) to %s.", url, to)
40
+ with requests.get(url, stream=True, timeout=10) as response:
41
+ response.raise_for_status()
42
+ total_size = int(response.headers.get("content-length", 0))
43
+ with (
44
+ to.open("wb") as f,
45
+ tqdm(
46
+ # Make the URL less verbose in the progress bar
47
+ desc=url.replace("https://s3.amazonaws.com/session-manager-downloads/plugin", "..."),
48
+ total=total_size,
49
+ unit="iB",
50
+ unit_scale=True,
51
+ unit_divisor=1_024,
52
+ ) as pbar,
53
+ ):
54
+ for chunk in response.iter_content(chunk_size=8_192):
55
+ size = f.write(chunk)
56
+ pbar.update(size)
57
+
58
+ return to.absolute()
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def command_as_root(command: list[str], *, root: bool | None = None) -> list[str]:
8
+ """Modify a command to run as root (`sudo`) if not already running as root."""
9
+ root = root or is_root()
10
+ if not root:
11
+ command = ["sudo", *command]
12
+
13
+ return command
14
+
15
+
16
+ def is_root() -> bool:
17
+ """Check if the current user is root."""
18
+ return os.geteuid() == 0
19
+
20
+
21
+ def os_release() -> dict[str, str]:
22
+ """Parse `/etc/os-release` file into a dictionary."""
23
+ content = Path("/etc/os-release").read_text()
24
+ return {
25
+ key.strip('"'): value.strip('"')
26
+ for key, value in (line.split("=", 1) for line in content.splitlines() if "=" in line)
27
+ }
@@ -0,0 +1,133 @@
1
+ # flake8: noqa: B008
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any, TypedDict
6
+
7
+ import boto3
8
+
9
+ # Type aliases for readability
10
+ _ARN = str
11
+ _Variables = dict[str, Any]
12
+
13
+ # TODO(lasuillard): Need some refactoring (with #2, #3)
14
+ # TODO(lasuillard): Put some logging
15
+
16
+
17
+ class _LoadStatsDict(TypedDict):
18
+ secrets: int
19
+ parameters: int
20
+
21
+
22
+ class VariableLoader: # noqa: D101
23
+ def __init__(self, *, dry_run: bool) -> None:
24
+ """Initialize the VariableLoader.
25
+
26
+ Args:
27
+ dry_run: Whether to run in dry-run mode.
28
+ console: Rich console instance.
29
+ """
30
+ self.dry_run = dry_run
31
+
32
+ # TODO(lasuillard): Currently not using pagination (do we need more than 10-20 secrets or parameters each?)
33
+ # ; consider adding it if needed
34
+ def load(self, map_arns: dict[str, _ARN]) -> tuple[dict[str, Any], _LoadStatsDict]:
35
+ """Load the variables from the AWS Secrets Manager and SSM Parameter Store.
36
+
37
+ Each secret or parameter should be a valid dictionary, where the keys are the variable names
38
+ and the values are the variable values.
39
+
40
+ The items are merged in the order of the key of provided mapping, overwriting the variables with the same name
41
+ in the order of the keys.
42
+ """
43
+ # Split the ARNs by resource types
44
+ secrets_map, parameters_map = {}, {}
45
+ for idx, arn in map_arns.items():
46
+ if arn.startswith("arn:aws:secretsmanager:"):
47
+ secrets_map[idx] = arn
48
+ elif arn.startswith("arn:aws:ssm:"):
49
+ parameters_map[idx] = arn
50
+ else:
51
+ msg = f"Unsupported resource: {arn!r}"
52
+ raise ValueError(msg)
53
+
54
+ # Retrieve variables from AWS resources
55
+ secrets: dict[str, _Variables]
56
+ parameters: dict[str, _Variables]
57
+ if self.dry_run:
58
+ secrets = {idx: {} for idx, _ in secrets_map.items()}
59
+ parameters = {idx: {} for idx, _ in parameters_map.items()}
60
+ else:
61
+ secrets = self._retrieve_secrets(secrets_map)
62
+ parameters = self._retrieve_parameters(parameters_map)
63
+
64
+ load_stats: _LoadStatsDict = {
65
+ "secrets": len(secrets),
66
+ "parameters": len(parameters),
67
+ }
68
+
69
+ # Merge the variables in order
70
+ full_variables = secrets | parameters # Keys MUST NOT conflict
71
+ merged_in_order = {}
72
+ for _, variables in sorted(full_variables.items()):
73
+ merged_in_order.update(variables)
74
+
75
+ return merged_in_order, load_stats
76
+
77
+ def _retrieve_secrets(self, secrets_map: dict[str, _ARN]) -> dict[str, _Variables]:
78
+ """Retrieve the secrets from AWS Secrets Manager."""
79
+ if not secrets_map:
80
+ return {}
81
+
82
+ secretsmanager = boto3.client("secretsmanager")
83
+
84
+ # Retrieve the secrets
85
+ arns = list(secrets_map.values())
86
+ response = secretsmanager.batch_get_secret_value(SecretIdList=arns)
87
+ if errors := response["Errors"]:
88
+ msg = f"Failed to retrieve secrets: {errors!r}"
89
+ raise ValueError(msg)
90
+
91
+ # Parse the secrets
92
+ secrets = response["SecretValues"]
93
+ result = {}
94
+ for secret in secrets:
95
+ arn = secret["ARN"]
96
+ order_key = next(key for key, value in secrets_map.items() if value == arn)
97
+ data = json.loads(secret["SecretString"])
98
+ if not isinstance(data, dict):
99
+ msg = f"Secret data must be a valid dictionary, but got: {type(data)!r}"
100
+ raise TypeError(msg)
101
+
102
+ result[order_key] = data
103
+
104
+ return result
105
+
106
+ def _retrieve_parameters(self, parameters_map: dict[str, _ARN]) -> dict[str, _Variables]:
107
+ """Retrieve the parameters from AWS SSM Parameter Store."""
108
+ if not parameters_map:
109
+ return {}
110
+
111
+ ssm = boto3.client("ssm")
112
+
113
+ # Retrieve the parameters
114
+ parameter_names = list(parameters_map.values())
115
+ response = ssm.get_parameters(Names=parameter_names, WithDecryption=True)
116
+ if errors := response["InvalidParameters"]:
117
+ msg = f"Failed to retrieve parameters: {errors!r}"
118
+ raise ValueError(msg)
119
+
120
+ # Parse the parameters
121
+ parameters = response["Parameters"]
122
+ result = {}
123
+ for parameter in parameters:
124
+ arn = parameter["ARN"]
125
+ order_key = next(key for key, value in parameters_map.items() if value == arn)
126
+ data = json.loads(parameter["Value"])
127
+ if not isinstance(data, dict):
128
+ msg = f"Parameter data must be a valid dictionary, but got: {type(data)!r}"
129
+ raise TypeError(msg)
130
+
131
+ result[order_key] = data
132
+
133
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-annoying
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Utils to handle some annoying AWS tasks.
5
5
  Project-URL: Homepage, https://github.com/lasuillard/aws-annoying
6
6
  Project-URL: Repository, https://github.com/lasuillard/aws-annoying.git
@@ -9,13 +9,16 @@ Author-email: Yuchan Lee <lasuillard@gmail.com>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: <4.0,>=3.9
12
- Requires-Dist: boto3>=1.37.1
13
- Requires-Dist: pydantic>=2.10.6
14
- Requires-Dist: typer>=0.15.1
12
+ Requires-Dist: boto3<2,>=1
13
+ Requires-Dist: pydantic<3,>=2
14
+ Requires-Dist: requests<3,>=2
15
+ Requires-Dist: tqdm<5,>=4
16
+ Requires-Dist: typer<1,>=0
15
17
  Provides-Extra: dev
16
- Requires-Dist: boto3-stubs[ecs,secretsmanager,ssm,sts]>=1.37.1; extra == 'dev'
18
+ Requires-Dist: boto3-stubs[ec2,ecs,secretsmanager,ssm,sts]>=1.37.1; extra == 'dev'
17
19
  Requires-Dist: mypy~=1.15.0; extra == 'dev'
18
20
  Requires-Dist: ruff<0.12.0,>=0.9.9; extra == 'dev'
21
+ Requires-Dist: types-requests>=2.31.0.6; extra == 'dev'
19
22
  Provides-Extra: test
20
23
  Requires-Dist: coverage<7.9,>=7.6; extra == 'test'
21
24
  Requires-Dist: moto[ecs,secretsmanager,server,ssm]~=5.1.1; extra == 'test'
@@ -0,0 +1,30 @@
1
+ aws_annoying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ aws_annoying/mfa.py,sha256=m6-V1bWeUWsAmRddl-lv13mPCMnftoPzJoNnZ0kiaWQ,2007
3
+ aws_annoying/variables.py,sha256=a9cMS9JU-XA2h1tztO7ofixoDEpqtS_eVEiWrQ75mTo,4761
4
+ aws_annoying/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ aws_annoying/cli/app.py,sha256=sp50uVoAl4D6Wk3DFpzKZzSsxmSxNYejFxm62b_Kxps,201
6
+ aws_annoying/cli/ecs_task_definition_lifecycle.py,sha256=O36Bf5LBnVJNyYmdlUxhtsIHNoxky1t5YacAXiL9UEI,2803
7
+ aws_annoying/cli/load_variables.py,sha256=eWNByUEc1ijF8uCe_egdAnjWxfMNCZeVr0vtTtQLe3Y,5086
8
+ aws_annoying/cli/main.py,sha256=TSzPeMkgIgKFf3bom_vDkFYK0bHF1r5K9ADreZUV3k4,503
9
+ aws_annoying/cli/mfa/__init__.py,sha256=rbEGhw5lOQZV_XAc3nSbo56JVhsSPpeOgEtiAy9qzEA,50
10
+ aws_annoying/cli/mfa/_app.py,sha256=Ub7gxb6kGF3Ve1ucQSOjHmc4jAu8mxgegcXsIbOzLLQ,189
11
+ aws_annoying/cli/mfa/configure.py,sha256=vsoHfTVFF2dPgiYsp2L-EkMwtAA0_-tVwFd6Wv6DscU,3746
12
+ aws_annoying/cli/session_manager/__init__.py,sha256=FkT6jT6OXduOURN61d-U6hgd-XluQbvuVtKXXiXgSEk,105
13
+ aws_annoying/cli/session_manager/_app.py,sha256=OVOHW0iyKzunvaqLhjoseHw1-WxJ1gGb7QmiyAEezyY,221
14
+ aws_annoying/cli/session_manager/_common.py,sha256=0fCm6Zx6P1NcyyiHlSoB9PgIdrxzUXLVPqKJWQnJ8I4,792
15
+ aws_annoying/cli/session_manager/install.py,sha256=zX2cu-IBYlf9yl8z9P0CTQz72BP6aAMo-KnolC_PGsU,1443
16
+ aws_annoying/cli/session_manager/port_forward.py,sha256=uMcsafTNAHZh-0E1yWXCliMivWyuWV2K9DdVhBZ9pG8,4327
17
+ aws_annoying/cli/session_manager/start.py,sha256=1yaMuy-7IWrrDoPHjgygOP_-g_tajfnpaVPZanxsZxs,215
18
+ aws_annoying/cli/session_manager/stop.py,sha256=ttU6nlbVgBkZDtY-DwUyCstv5TFtat5TljkyuY8QICU,1482
19
+ aws_annoying/session_manager/__init__.py,sha256=swbRdFh9CdO6tzyBjSxN0KC7MMASvptIpbsijUPIZgI,243
20
+ aws_annoying/session_manager/errors.py,sha256=YioKlRtZ-GUP0F_ts_ebw7-HYkxe8mTes6HK821Kuiw,353
21
+ aws_annoying/session_manager/session_manager.py,sha256=7tqfFPA4bWF0Me_VbUDcq5Cpl1sw3bevGyT8QWfLTOk,12454
22
+ aws_annoying/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ aws_annoying/utils/debugger.py,sha256=UFllDCGI2gPtwo1XS5vqw0qyR6bYr7XknmBwSxalKIc,754
24
+ aws_annoying/utils/downloader.py,sha256=aB5RzT-LpbFX24-2HXlAkdgVowc4TR9FWT_K8WwZ1BE,1923
25
+ aws_annoying/utils/platform.py,sha256=h3DUWmTMM-_4TfTWNqY0uNqyVsBjAuMm2DEbG-daxe8,742
26
+ aws_annoying-0.4.0.dist-info/METADATA,sha256=9u5wofuLZXXPrAj1YcOhbqPlPH46jtub8AUvs2KNZ9s,1916
27
+ aws_annoying-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
+ aws_annoying-0.4.0.dist-info/entry_points.txt,sha256=DcKE5V0WvVJ8wUOHxyUz1yLAJOuuJUgRPlMcQ4O7jEs,66
29
+ aws_annoying-0.4.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
30
+ aws_annoying-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aws-annoying = aws_annoying.cli.main:entrypoint