nate2-scripts 1.0.1__tar.gz → 1.0.3__tar.gz

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 (19) hide show
  1. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/PKG-INFO +2 -2
  2. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/README.md +14 -11
  3. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/pyproject.toml +4 -1
  4. nate2_scripts-1.0.1/src/nate2_scripts/bing_wrapper.py → nate2_scripts-1.0.3/src/nate2_scripts/bing_wrapper/__init__.py +41 -0
  5. nate2_scripts-1.0.3/src/nate2_scripts/bing_wrapper/autocomplete.sh +30 -0
  6. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/__init__.py +51 -14
  7. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/destitationResolver.py +3 -0
  8. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/nameResolver.py +12 -5
  9. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/resourceIdResolver.py +10 -8
  10. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/sessionCache.py +5 -1
  11. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/.github/workflows/publish.yml +0 -0
  12. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/.gitignore +0 -0
  13. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/CONSOLE-NOTES.md +0 -0
  14. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/LICENSE +0 -0
  15. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/__init__.py +0 -0
  16. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/arnResolver.py +0 -0
  17. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/hashGrabber.py +0 -0
  18. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/console/regionCache.py +0 -0
  19. {nate2_scripts-1.0.1 → nate2_scripts-1.0.3}/src/nate2_scripts/test.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: nate2-scripts
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Utility scripts for AWS development on macOS
5
5
  Project-URL: Homepage, https://github.com/tennex-nwestfall/nate2-scripts
6
6
  License-File: LICENSE
@@ -1,4 +1,3 @@
1
-
2
1
  [![PyPI version](https://img.shields.io/pypi/v/nate2-scripts)](https://pypi.org/project/nate2-scripts/)
3
2
  [![License](https://img.shields.io/github/license/tennex-nwestfall/nate2-scripts)](https://github.com/tennex-nwestfall/nate2-scripts/blob/main/LICENSE)
4
3
 
@@ -35,7 +34,7 @@ eval "$(register-python-argcomplete console)"
35
34
  **Usage:**
36
35
 
37
36
  ```bash
38
- # change to an account/role
37
+ # change to an account/role
39
38
  asp my-aws-role
40
39
 
41
40
  # open main page of the role/account
@@ -46,8 +45,8 @@ console ec2
46
45
  console s3
47
46
  # open lambda page of the role/account in us-west-2
48
47
  console lambda -r us-west-2
49
- # open instance id page of the role/account in
50
- # whatever region that instance is in
48
+ # open instance id page of the role/account in
49
+ # whatever region that instance is in
51
50
  console i-0123456789abcdef
52
51
  # open the console page for the resource the ARN points to
53
52
  console arn:aws:ec2:us-east-1:012345678967:instance/i-0123456789abcdef
@@ -60,7 +59,7 @@ console arn:aws:ec2:us-east-1:012345678967:instance/i-0123456789abcdef
60
59
  **Supported ARNs:** `ec2`, `iam` (role, user), `lambda`, `logs` (CloudWatch Logs), `rds`, `s3`, `secretsmanager`, `ecs`, `eks`, `sns`, `sqs`, `batch`, `dynamodb`, `states` (Step Functions), `elasticloadbalancing`
61
60
 
62
61
  **All Regions Supported:**
63
- Edit `console-regions.json` to change which regions are evaluated and to set the default one
62
+ Edit `~/.local/state/console-regions.json` to change which regions are evaluated and to set the default one
64
63
 
65
64
  ---
66
65
 
@@ -78,13 +77,17 @@ alias cdk="bing-wrapper.py --end --pattern \"(y/n)\" --sound-end /System/Library
78
77
 
79
78
  # wraps nextflow to play a sound when done
80
79
  alias nextflow="bing-wrapper.py --end -- nextflow"
80
+
81
+ # needed for autocomplete of bing-wrapper and the inner command
82
+ eval "$(bing-wrapper --autocomplete)"
81
83
  ```
82
84
 
83
85
  **Options:**
84
86
 
85
- | Flag | Description | Default |
86
- | --- | --- | --- |
87
- | `--end` | Play a sound when the command exits | off |
88
- | `--pattern <text>` | Play a sound when `<text>` appears in output (repeatable) | none |
89
- | `--sound-end <path>` | Sound file to play on exit | `Glass.aiff` |
90
- | `--sound-pattern <path>` | Sound file to play on pattern match | `Glass.aiff` |
87
+ | Flag | Description | Default |
88
+ | ------------------------ | --------------------------------------------------------- | ------------ |
89
+ | `--end` | Play a sound when the command exits | off |
90
+ | `--pattern <text>` | Play a sound when `<text>` appears in output (repeatable) | none |
91
+ | `--sound-end <path>` | Sound file to play on exit | `Glass.aiff` |
92
+ | `--sound-pattern <path>` | Sound file to play on pattern match | `Glass.aiff` |
93
+ | `--sound-while <path>` | Sound file to play on while the command runs | none |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nate2-scripts"
7
- version = "1.0.1"
7
+ version = "1.0.3"
8
8
  description = "Utility scripts for AWS development on macOS"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -19,5 +19,8 @@ bing-wrapper = "nate2_scripts.bing_wrapper:main"
19
19
  [tool.hatch.build.targets.wheel]
20
20
  packages = ["src/nate2_scripts"]
21
21
 
22
+ [tool.hatch.build.targets.wheel.force-include]
23
+ "src/nate2_scripts/bing_wrapper/autocomplete.sh" = "nate2_scripts/bing_wrapper/autocomplete.sh"
24
+
22
25
  [project.urls]
23
26
  Homepage = "https://github.com/tennex-nwestfall/nate2-scripts"
@@ -8,6 +8,7 @@ import struct
8
8
  import subprocess
9
9
  import sys
10
10
  import termios
11
+ import threading
11
12
  import tty
12
13
  import argparse
13
14
 
@@ -55,6 +56,26 @@ def run(args: argparse.Namespace) -> None:
55
56
  # ---- parent ----
56
57
  os.close(slave_fd)
57
58
 
59
+ # Loop sound-while for the lifetime of the child process.
60
+ _stop_sound_while = threading.Event()
61
+ _sound_while_thread = None
62
+
63
+ def _play_sound_while_loop() -> None:
64
+ while not _stop_sound_while.is_set():
65
+ proc = subprocess.Popen(["afplay", args.sound_while])
66
+ while proc.poll() is None:
67
+ if _stop_sound_while.wait(timeout=0.05):
68
+ proc.terminate()
69
+ try:
70
+ proc.wait(timeout=1)
71
+ except subprocess.TimeoutExpired:
72
+ proc.kill()
73
+ return
74
+
75
+ if args.sound_while:
76
+ _sound_while_thread = threading.Thread(target=_play_sound_while_loop, daemon=True)
77
+ _sound_while_thread.start()
78
+
58
79
  # Propagate terminal resizes to the child (also sends SIGWINCH to it).
59
80
  def _sigwinch(signum: int, frame) -> None:
60
81
  try:
@@ -119,6 +140,10 @@ def run(args: argparse.Namespace) -> None:
119
140
 
120
141
  _, status = os.waitpid(pid, 0)
121
142
 
143
+ _stop_sound_while.set()
144
+ if _sound_while_thread is not None:
145
+ _sound_while_thread.join(timeout=2)
146
+
122
147
  if args.end:
123
148
  subprocess.Popen(["afplay", args.sound_end])
124
149
  if os.WIFEXITED(status):
@@ -156,9 +181,25 @@ def main() -> None:
156
181
  type=str,
157
182
  default="/System/Library/Sounds/Glass.aiff",
158
183
  )
184
+ parser.add_argument(
185
+ "--sound-while",
186
+ help="play sound while command is running",
187
+ type=str,
188
+ default="",
189
+ )
190
+ parser.add_argument(
191
+ "--autocomplete",
192
+ help="return autocomplete scripts",
193
+ action="store_true",
194
+ )
159
195
 
160
196
  args = parser.parse_args()
161
197
 
198
+ if args.autocomplete:
199
+ script = os.path.join(os.path.dirname(__file__), "autocomplete.sh")
200
+ print(open(script).read())
201
+ sys.exit(0)
202
+
162
203
  if not args.command:
163
204
  parser.print_help()
164
205
  sys.exit(1)
@@ -0,0 +1,30 @@
1
+ _bing_wrapper() {
2
+ local -a bing_args
3
+ local -i double_dash_pos=0
4
+
5
+ # Find position of "--"
6
+ for (( i=1; i <= $#words; i++ )); do
7
+ if [[ "${words[$i]}" == "--" ]]; then
8
+ double_dash_pos=$i
9
+ break
10
+ fi
11
+ done
12
+
13
+ if (( double_dash_pos > 0 && CURRENT > double_dash_pos )); then
14
+ # Delegate to the wrapped command's completion
15
+ words=("${words[@]:$double_dash_pos}")
16
+ CURRENT=$(( CURRENT - double_dash_pos ))
17
+ _normal # let zsh look up the right completion for inner_cmd
18
+ else
19
+ # Complete bing-wrapper's own options
20
+ _arguments \
21
+ '--end[play sound at end]' \
22
+ '*--pattern[pattern to listen for]:pattern:' \
23
+ '--sound-end[sound file at end]:file:_files' \
24
+ '--sound-pattern[sound file for pattern]:file:_files' \
25
+ '--sound-while[sound file for duration of command]:file:_files' \
26
+ '--[inner command]:command:_normal' \
27
+ fi
28
+ }
29
+
30
+ compdef _bing_wrapper bing-wrapper
@@ -21,21 +21,55 @@ from nate2_scripts.console.hashGrabber import HashGrabber
21
21
  from nate2_scripts.console.regionCache import RegionCache
22
22
  from nate2_scripts.console.sessionCache import SessionCache
23
23
 
24
-
25
- def get_account_id() -> str:
26
- return boto3.client("sts").get_caller_identity()["Account"]
24
+ sts = boto3.client("sts")
27
25
 
28
26
 
29
- def get_credentials() -> dict:
27
+ def get_email() -> str:
30
28
  result = subprocess.run(
31
- ["aws", "configure", "export-credentials"],
32
- capture_output=True,
33
- text=True,
29
+ ["git", "config", "user.email"], capture_output=True, text=True
34
30
  )
35
- if result.returncode != 0:
36
- print(f"Error: Failed to export credentials.\n{result.stderr}", file=sys.stderr)
31
+ return result.stdout.strip()
32
+
33
+
34
+ def get_account_id() -> str:
35
+ return sts.get_caller_identity()["Account"]
36
+
37
+
38
+ def get_credentials(account_id: str) -> dict:
39
+ # Check if already in an assumed role with the correct session name
40
+ identity = sts.get_caller_identity()
41
+ arn = identity.get("Arn", "")
42
+ email = get_email()
43
+ if f"assumed-role/" in arn and arn.endswith(f"/{email}"):
44
+ result = subprocess.run(
45
+ ["aws", "configure", "export-credentials"],
46
+ capture_output=True,
47
+ text=True,
48
+ )
49
+ if result.returncode == 0:
50
+ return json.loads(result.stdout)
51
+
52
+ profile = os.environ.get("AWS_PROFILE", "default")
53
+ session = boto3.Session(profile_name=profile)
54
+ config = session._session.get_scoped_config()
55
+
56
+ role_arn = config.get("role_arn", f"arn:aws:iam::{account_id}:role/OrganizationAccountAccessRole")
57
+ source_profile = config.get("source_profile")
58
+
59
+ if source_profile:
60
+ source_sts = boto3.Session(profile_name=source_profile).client("sts")
61
+ else:
62
+ source_sts = sts
63
+
64
+ try:
65
+ response = source_sts.assume_role(
66
+ RoleArn=role_arn,
67
+ RoleSessionName=email,
68
+ )
69
+ except Exception as e:
70
+ print(f"Error: Failed to assume role {role_arn}.\n{e}", file=sys.stderr)
37
71
  sys.exit(1)
38
- return json.loads(result.stdout)
72
+ return response["Credentials"]
39
73
 
40
74
 
41
75
  def get_signin_token(creds: dict) -> str:
@@ -115,15 +149,15 @@ def parse_args(region_cache: RegionCache) -> argparse.Namespace:
115
149
 
116
150
 
117
151
  def main() -> None:
118
- profile_name = get_profile()
119
152
  region_cache = RegionCache()
153
+ args = parse_args(region_cache)
154
+
155
+ profile_name = get_profile()
120
156
  session_cache = SessionCache()
121
157
 
122
158
  print("Loading regions from cache:", region_cache.get_regions())
123
159
  print("Loading default region from cache:", region_cache.get_default_region())
124
160
 
125
- args = parse_args(region_cache)
126
-
127
161
  if args.force:
128
162
  print("Force passed. Reseting session cache.")
129
163
  session_cache.reset()
@@ -131,8 +165,8 @@ def main() -> None:
131
165
  if args.region:
132
166
  print("Default region is now:", args.region)
133
167
 
134
- creds = get_credentials()
135
168
  account_id = get_account_id()
169
+ creds = get_credentials(account_id)
136
170
  hash = session_cache.get_session_hash(profile_name, creds)
137
171
 
138
172
  multisession_domain_name = ""
@@ -172,3 +206,6 @@ def main() -> None:
172
206
 
173
207
  if __name__ == "__main__":
174
208
  main()
209
+
210
+
211
+ # set user to OrganizationAccountAccessRole/nwestfall@tennex.io
@@ -107,6 +107,9 @@ class DestinationResolver:
107
107
  "ct": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
108
108
  "fsx": f"https://{mdn}{region}.console.aws.amazon.com/fsx/",
109
109
  "subnet": f"https://{mdn}{region}.console.aws.amazon.com/vpcconsole/home#subnets:",
110
+ "ssm": f"https://{mdn}{region}.console.aws.amazon.com/systems-manager/",
111
+ "parameter": f"https://{mdn}{region}.console.aws.amazon.com/systems-manager/parameters/",
112
+ "ps": f"https://{mdn}{region}.console.aws.amazon.com/systems-manager/parameters/",
110
113
  # unchecked
111
114
  }
112
115
 
@@ -21,13 +21,14 @@ class NameResolver:
21
21
  def resolve_name(self, name: str) -> str | None:
22
22
  regions = self.region_cache.get_regions()
23
23
 
24
- # Priority: 0=logs, 1=ec2, 2=s3, 3=cloudformation
24
+ # Priority: 0=logs, 1=ec2, 2=s3, 3=cloudformation, 4=lambda
25
25
  tasks: list[tuple] = []
26
26
  for region in regions:
27
27
  tasks.append((0, self._resolve_log_group, name, region))
28
- tasks.append((1, self._resolve_ec2_instance, name, region))
28
+ tasks.append((2, self._resolve_ec2_instance, name, region))
29
29
  tasks.append((3, self._resolve_cloudformation_stack, name, region))
30
- tasks.append((2, self._resolve_s3_bucket, name))
30
+ tasks.append((4, self._resolve_lambda_function, name, region))
31
+ tasks.append((1, self._resolve_s3_bucket, name))
31
32
 
32
33
  results: dict[int, str] = {}
33
34
 
@@ -94,6 +95,12 @@ class NameResolver:
94
95
  except Exception:
95
96
  pass
96
97
  return None
97
-
98
-
99
98
 
99
+ def _resolve_lambda_function(self, name: str, region: str) -> str | None:
100
+ try:
101
+ boto3.client("lambda", region_name=region).get_function(FunctionName=name)
102
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com"
103
+ return f"{base}/lambda/home?region={region}#/functions/{name}"
104
+ except ClientError:
105
+ pass
106
+ return None
@@ -77,14 +77,16 @@ class ResourceIdResolver:
77
77
  # keeping it here in case I figure out later
78
78
  # if re.match(r"^fl-[0-9a-f]{8,17}$", resource_id):
79
79
  # return "fl"
80
- if self._KMS_UUID.match(resource_id) or self._KMS_MRK.match(resource_id):
81
- return "kms"
82
- if resource_id.startswith("db/"):
83
- return "rds"
84
- if resource_id.startswith("role/"):
85
- return "role"
86
- if resource_id.startswith("usr/"):
87
- return "user"
80
+
81
+ # not tested
82
+ # if self._KMS_UUID.match(resource_id) or self._KMS_MRK.match(resource_id):
83
+ # return "kms"
84
+ # if resource_id.startswith("db/"):
85
+ # return "rds"
86
+ # if resource_id.startswith("role/"):
87
+ # return "role"
88
+ # if resource_id.startswith("usr/"):
89
+ # return "user"
88
90
  return None
89
91
 
90
92
  def _resolve_in_region(
@@ -59,9 +59,13 @@ class SessionCache:
59
59
 
60
60
  # Honour the STS credential expiration if present
61
61
  expiration = creds.get("Expiration")
62
+ print(expiration)
62
63
  if expiration:
63
64
  try:
64
- exp_time = datetime.fromisoformat(expiration.replace("Z", "+00:00"))
65
+ if isinstance(expiration, datetime):
66
+ exp_time = expiration
67
+ else:
68
+ exp_time = datetime.fromisoformat(expiration.replace("Z", "+00:00"))
65
69
  if datetime.now(timezone.utc) >= exp_time:
66
70
  return None
67
71
  except ValueError:
File without changes
File without changes