nate2-scripts 1.0.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.
@@ -0,0 +1,4 @@
1
+ import sys
2
+
3
+ if sys.platform != "darwin":
4
+ raise RuntimeError("nate2-scripts only supports macOS")
@@ -0,0 +1,170 @@
1
+ """PTY wrapper for cdk that plays a sound when (y/n) prompts appear."""
2
+
3
+ import fcntl
4
+ import os
5
+ import select
6
+ import signal
7
+ import struct
8
+ import subprocess
9
+ import sys
10
+ import termios
11
+ import tty
12
+ import argparse
13
+
14
+ # _PATTERN = b"(y/n)"
15
+ # _WINDOW = len(_PATTERN) * 2
16
+
17
+
18
+ def _get_winsize(fd: int) -> bytes:
19
+ return fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 8)
20
+
21
+
22
+ def _set_winsize(fd: int, winsize: bytes) -> None:
23
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
24
+
25
+
26
+ def run(args: argparse.Namespace) -> None:
27
+ patterns = [p.encode() for p in args.patterns] if args.patterns else []
28
+ window = max(map(len, patterns)) * 2 if patterns else 1
29
+ # Capture real terminal dimensions before creating the PTY.
30
+ # pty.spawn() leaves the PTY at {0,0,0,0}, which makes CDK format
31
+ # output to width 1. Using os.openpty() lets us set the size before
32
+ # forking so cdk reads correct dimensions from its very first TIOCGWINSZ.
33
+ try:
34
+ winsize = _get_winsize(sys.stdout.fileno())
35
+ except OSError:
36
+ winsize = struct.pack("HHHH", 24, 80, 0, 0)
37
+
38
+ master_fd, slave_fd = os.openpty()
39
+ _set_winsize(master_fd, winsize)
40
+
41
+ pid = os.fork()
42
+ if pid == 0:
43
+ # ---- child ----
44
+ os.close(master_fd)
45
+ os.setsid()
46
+ # Acquire slave as the controlling terminal.
47
+ fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
48
+ for fd in (0, 1, 2):
49
+ os.dup2(slave_fd, fd)
50
+ if slave_fd > 2:
51
+ os.close(slave_fd)
52
+ os.execvp(args.command[0], args.command)
53
+ os._exit(1)
54
+
55
+ # ---- parent ----
56
+ os.close(slave_fd)
57
+
58
+ # Propagate terminal resizes to the child (also sends SIGWINCH to it).
59
+ def _sigwinch(signum: int, frame) -> None:
60
+ try:
61
+ _set_winsize(master_fd, _get_winsize(sys.stdout.fileno()))
62
+ except OSError:
63
+ pass
64
+
65
+ signal.signal(signal.SIGWINCH, _sigwinch)
66
+
67
+ stdin_fd = sys.stdin.fileno()
68
+ old_settings = None
69
+ try:
70
+ old_settings = termios.tcgetattr(stdin_fd)
71
+ tty.setraw(stdin_fd)
72
+ except termios.error:
73
+ pass
74
+
75
+ buf = b""
76
+ try:
77
+ while True:
78
+ try:
79
+ r, _, _ = select.select([master_fd, stdin_fd], [], [])
80
+ except (OSError, ValueError):
81
+ break
82
+
83
+ if master_fd in r:
84
+ try:
85
+ data = os.read(master_fd, 1024)
86
+ except OSError:
87
+ break
88
+ if not data:
89
+ break
90
+
91
+ check = buf + data
92
+ for pattern in patterns:
93
+ if pattern in check:
94
+ subprocess.Popen(["afplay", args.sound_pattern])
95
+ buf = b""
96
+ break
97
+
98
+ buf = check[-window:]
99
+
100
+ sys.stdout.buffer.write(data)
101
+ sys.stdout.buffer.flush()
102
+
103
+ if stdin_fd in r:
104
+ try:
105
+ data = os.read(stdin_fd, 1024)
106
+ except OSError:
107
+ break
108
+ if data:
109
+ try:
110
+ os.write(master_fd, data)
111
+ except OSError:
112
+ break
113
+ finally:
114
+ if old_settings is not None:
115
+ try:
116
+ termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
117
+ except termios.error:
118
+ pass
119
+
120
+ _, status = os.waitpid(pid, 0)
121
+
122
+ if args.end:
123
+ subprocess.Popen(["afplay", args.sound_end])
124
+ if os.WIFEXITED(status):
125
+ sys.exit(os.WEXITSTATUS(status))
126
+ elif os.WIFSIGNALED(status):
127
+ sys.exit(128 + os.WTERMSIG(status))
128
+ else:
129
+ sys.exit(1)
130
+
131
+
132
+ def main() -> None:
133
+ parser = argparse.ArgumentParser(
134
+ description="PTY wrapper for cdk that plays a sound when (y/n) prompts appear."
135
+ )
136
+ parser.add_argument("command", nargs="*", help="Command to run")
137
+ parser.add_argument(
138
+ "--end", help="make sound at end of command", action="store_true"
139
+ )
140
+ parser.add_argument(
141
+ "--pattern",
142
+ help="make sound when pattern in seen",
143
+ type=str,
144
+ action="append",
145
+ dest="patterns",
146
+ )
147
+ parser.add_argument(
148
+ "--sound-end",
149
+ help="change the sound at end of command",
150
+ type=str,
151
+ default="/System/Library/Sounds/Glass.aiff",
152
+ )
153
+ parser.add_argument(
154
+ "--sound-pattern",
155
+ help="change the sound when pattern is seen",
156
+ type=str,
157
+ default="/System/Library/Sounds/Glass.aiff",
158
+ )
159
+
160
+ args = parser.parse_args()
161
+
162
+ if not args.command:
163
+ parser.print_help()
164
+ sys.exit(1)
165
+
166
+ run(args)
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,174 @@
1
+ # PYTHON_ARGCOMPLETE_OK
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ import urllib.parse
10
+ import webbrowser
11
+ import argparse
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ import argcomplete
16
+ import boto3
17
+
18
+ from nate2_scripts.console.destitationResolver import DestinationResolver
19
+
20
+ from nate2_scripts.console.hashGrabber import HashGrabber
21
+ from nate2_scripts.console.regionCache import RegionCache
22
+ from nate2_scripts.console.sessionCache import SessionCache
23
+
24
+
25
+ def get_account_id() -> str:
26
+ return boto3.client("sts").get_caller_identity()["Account"]
27
+
28
+
29
+ def get_credentials() -> dict:
30
+ result = subprocess.run(
31
+ ["aws", "configure", "export-credentials"],
32
+ capture_output=True,
33
+ text=True,
34
+ )
35
+ if result.returncode != 0:
36
+ print(f"Error: Failed to export credentials.\n{result.stderr}", file=sys.stderr)
37
+ sys.exit(1)
38
+ return json.loads(result.stdout)
39
+
40
+
41
+ def get_signin_token(creds: dict) -> str:
42
+ session = json.dumps(
43
+ {
44
+ "sessionId": creds["AccessKeyId"],
45
+ "sessionKey": creds["SecretAccessKey"],
46
+ "sessionToken": creds["SessionToken"],
47
+ }
48
+ )
49
+ sess = urllib.parse.quote(session)
50
+ token_url = (
51
+ f"https://signin.aws.amazon.com/federation?Action=getSigninToken&Session={sess}"
52
+ )
53
+ result = subprocess.run(
54
+ ["curl", "-s", token_url], capture_output=True, text=True, check=True
55
+ )
56
+ try:
57
+ return json.loads(result.stdout)["SigninToken"]
58
+ except json.JSONDecodeError:
59
+ print("Error: Failed to parse JSON response. You may not have credentials.")
60
+ sys.exit(1)
61
+
62
+
63
+ def build_console_url(destination: str, signin_token: str) -> str:
64
+ dest = urllib.parse.quote(destination, safe="")
65
+ return f"https://signin.aws.amazon.com/federation?Action=login&Issuer=&Destination={dest}&SigninToken={signin_token}"
66
+
67
+
68
+ def get_profile() -> str:
69
+ profile_name = os.environ.get("AWS_PROFILE", "")
70
+ if not profile_name:
71
+ print(
72
+ "Error: AWS_PROFILE environment variable is not set. run asp.",
73
+ file=sys.stderr,
74
+ )
75
+ sys.exit(1)
76
+ return profile_name
77
+
78
+
79
+ def parse_args(region_cache: RegionCache) -> argparse.Namespace:
80
+ parser = argparse.ArgumentParser(
81
+ description="Open AWS console for a specific service."
82
+ )
83
+ svc_arg = parser.add_argument(
84
+ "service",
85
+ type=str,
86
+ default="",
87
+ nargs="?",
88
+ help="The AWS service to open in the console, or an AWS ARN.",
89
+ )
90
+ setattr(
91
+ svc_arg,
92
+ "completer",
93
+ argcomplete.completers.ChoicesCompleter(
94
+ list(DestinationResolver.get_service_list())
95
+ ),
96
+ )
97
+ parser.add_argument(
98
+ "-r",
99
+ "--region",
100
+ type=str,
101
+ default="",
102
+ choices=region_cache.get_regions(),
103
+ help="The AWS region to use.",
104
+ )
105
+ parser.add_argument(
106
+ "-f",
107
+ "--force",
108
+ action="store_true",
109
+ help="Force new federations. This will reset the federations cache.",
110
+ )
111
+ argcomplete.autocomplete(parser)
112
+ args = parser.parse_args()
113
+
114
+ return args
115
+
116
+
117
+ def main() -> None:
118
+ profile_name = get_profile()
119
+ region_cache = RegionCache()
120
+ session_cache = SessionCache()
121
+
122
+ print("Loading regions from cache:", region_cache.get_regions())
123
+ print("Loading default region from cache:", region_cache.get_default_region())
124
+
125
+ args = parse_args(region_cache)
126
+
127
+ if args.force:
128
+ print("Force passed. Reseting session cache.")
129
+ session_cache.reset()
130
+
131
+ if args.region:
132
+ print("Default region is now:", args.region)
133
+
134
+ creds = get_credentials()
135
+ account_id = get_account_id()
136
+ hash = session_cache.get_session_hash(profile_name, creds)
137
+
138
+ multisession_domain_name = ""
139
+ signin_token = None
140
+ if hash is None:
141
+ print(f"{profile_name} has no cached session. Grabbing signin token.")
142
+ signin_token = get_signin_token(creds)
143
+ elif len(hash) == 0:
144
+ print(f"{profile_name} cache is single session")
145
+ else:
146
+ multisession_domain_name = f"{account_id}-{hash}."
147
+ print(
148
+ f"{profile_name} cache is multi session with domain: {multisession_domain_name}"
149
+ )
150
+
151
+ resolver = DestinationResolver(account_id, region_cache, multisession_domain_name)
152
+
153
+ destination = resolver.parse_destination(args.service, args.region)
154
+
155
+ print("parsed destination:", destination)
156
+
157
+ if signin_token is not None:
158
+ print("Building signin url")
159
+ console_url = build_console_url(destination, signin_token)
160
+ webbrowser.open(console_url)
161
+ start_time = time.time()
162
+ print(
163
+ "Waiting for up to 10 seconds to allow the browser to open before exiting..."
164
+ )
165
+ hash = HashGrabber.get_hash(account_id, start_time)
166
+ print(f"Hash found: {hash}")
167
+ session_cache.record_valid_hash(profile_name, hash)
168
+ else:
169
+ print("Opening url")
170
+ webbrowser.open(destination)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()