nate2-scripts 1.0.3__tar.gz → 2.0.0__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 (127) hide show
  1. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/.gitignore +2 -1
  2. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/PKG-INFO +5 -1
  3. nate2_scripts-2.0.0/README.md +204 -0
  4. nate2_scripts-2.0.0/i-want-to-refactor-fizzy-feather.md +173 -0
  5. nate2_scripts-2.0.0/plan.md +16 -0
  6. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/pyproject.toml +14 -1
  7. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/src/nate2_scripts/bing_wrapper/__init__.py +4 -2
  8. nate2_scripts-2.0.0/src/nate2_scripts/console/__init__.py +207 -0
  9. nate2_scripts-2.0.0/src/nate2_scripts/console/boto3_session.py +51 -0
  10. nate2_scripts-1.0.3/src/nate2_scripts/console/hashGrabber.py → nate2_scripts-2.0.0/src/nate2_scripts/console/hash_grabber.py +27 -12
  11. nate2_scripts-2.0.0/src/nate2_scripts/console/local_refresh.py +0 -0
  12. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/__init__.py +44 -0
  13. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/apigateway.py +11 -0
  14. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/artifact.py +11 -0
  15. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/athena.py +11 -0
  16. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/batch.py +52 -0
  17. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/bedrock.py +11 -0
  18. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudformation.py +28 -0
  19. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudfront.py +12 -0
  20. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudhsm.py +11 -0
  21. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudtrail.py +11 -0
  22. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cognito.py +11 -0
  23. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/config.py +11 -0
  24. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/controltower.py +11 -0
  25. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cost.py +13 -0
  26. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/dynamo.py +27 -0
  27. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ec2.py +118 -0
  28. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ecr.py +16 -0
  29. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ecs.py +61 -0
  30. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/efs.py +11 -0
  31. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/eks.py +11 -0
  32. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/elasticbeanstalk.py +11 -0
  33. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/elb.py +35 -0
  34. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/eventbridge.py +11 -0
  35. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/fsx.py +11 -0
  36. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/glue.py +11 -0
  37. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/guardduty.py +11 -0
  38. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/iam.py +43 -0
  39. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/inspector.py +11 -0
  40. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/kms.py +11 -0
  41. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/lambda_.py +45 -0
  42. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/lex.py +11 -0
  43. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/lightsail.py +11 -0
  44. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/logs.py +81 -0
  45. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/marketplace.py +24 -0
  46. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/polly.py +11 -0
  47. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/quick.py +15 -0
  48. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/rds.py +34 -0
  49. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/route53.py +16 -0
  50. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/s3.py +66 -0
  51. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/sagemaker.py +11 -0
  52. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/secretsmanager.py +31 -0
  53. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/sns.py +29 -0
  54. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/sqs.py +28 -0
  55. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ssm.py +18 -0
  56. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/stepfunctions.py +37 -0
  57. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/storagegateway.py +11 -0
  58. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/support.py +14 -0
  59. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/vpc.py +93 -0
  60. nate2_scripts-1.0.3/src/nate2_scripts/console/sessionCache.py → nate2_scripts-2.0.0/src/nate2_scripts/console/session_cache.py +8 -26
  61. nate2_scripts-2.0.0/src/nate2_scripts/console/text_resolver.py +193 -0
  62. nate2_scripts-2.0.0/src/nate2_scripts/console/types.py +59 -0
  63. {nate2_scripts-1.0.3/src/nate2_scripts → nate2_scripts-2.0.0}/test.py +4 -2
  64. nate2_scripts-2.0.0/tests/__init__.py +0 -0
  65. nate2_scripts-2.0.0/tests/console/__init__.py +0 -0
  66. nate2_scripts-2.0.0/tests/console/resolvers/__init__.py +29 -0
  67. nate2_scripts-2.0.0/tests/console/resolvers/apigateway.py +34 -0
  68. nate2_scripts-2.0.0/tests/console/resolvers/artifact.py +21 -0
  69. nate2_scripts-2.0.0/tests/console/resolvers/athena.py +34 -0
  70. nate2_scripts-2.0.0/tests/console/resolvers/batch.py +50 -0
  71. nate2_scripts-2.0.0/tests/console/resolvers/bedrock.py +36 -0
  72. nate2_scripts-2.0.0/tests/console/resolvers/cloudformation.py +132 -0
  73. nate2_scripts-2.0.0/tests/console/resolvers/cloudfront.py +34 -0
  74. nate2_scripts-2.0.0/tests/console/resolvers/cloudhsm.py +34 -0
  75. nate2_scripts-2.0.0/tests/console/resolvers/cloudtrail.py +34 -0
  76. nate2_scripts-2.0.0/tests/console/resolvers/cognito.py +34 -0
  77. nate2_scripts-2.0.0/tests/console/resolvers/config.py +34 -0
  78. nate2_scripts-2.0.0/tests/console/resolvers/conftest.py +40 -0
  79. nate2_scripts-2.0.0/tests/console/resolvers/controltower.py +21 -0
  80. nate2_scripts-2.0.0/tests/console/resolvers/cost.py +22 -0
  81. nate2_scripts-2.0.0/tests/console/resolvers/dynamo.py +47 -0
  82. nate2_scripts-2.0.0/tests/console/resolvers/ec2.py +269 -0
  83. nate2_scripts-2.0.0/tests/console/resolvers/ecr.py +34 -0
  84. nate2_scripts-2.0.0/tests/console/resolvers/ecs.py +60 -0
  85. nate2_scripts-2.0.0/tests/console/resolvers/efs.py +21 -0
  86. nate2_scripts-2.0.0/tests/console/resolvers/eks.py +21 -0
  87. nate2_scripts-2.0.0/tests/console/resolvers/elasticbeanstalk.py +34 -0
  88. nate2_scripts-2.0.0/tests/console/resolvers/elb.py +34 -0
  89. nate2_scripts-2.0.0/tests/console/resolvers/eventbridge.py +34 -0
  90. nate2_scripts-2.0.0/tests/console/resolvers/fsx.py +21 -0
  91. nate2_scripts-2.0.0/tests/console/resolvers/glue.py +21 -0
  92. nate2_scripts-2.0.0/tests/console/resolvers/guardduty.py +34 -0
  93. nate2_scripts-2.0.0/tests/console/resolvers/iam.py +21 -0
  94. nate2_scripts-2.0.0/tests/console/resolvers/inspector.py +21 -0
  95. nate2_scripts-2.0.0/tests/console/resolvers/kms.py +21 -0
  96. nate2_scripts-2.0.0/tests/console/resolvers/lambda_.py +202 -0
  97. nate2_scripts-2.0.0/tests/console/resolvers/lex.py +21 -0
  98. nate2_scripts-2.0.0/tests/console/resolvers/lightsail.py +34 -0
  99. nate2_scripts-2.0.0/tests/console/resolvers/logs.py +258 -0
  100. nate2_scripts-2.0.0/tests/console/resolvers/marketplace.py +60 -0
  101. nate2_scripts-2.0.0/tests/console/resolvers/polly.py +21 -0
  102. nate2_scripts-2.0.0/tests/console/resolvers/rds.py +21 -0
  103. nate2_scripts-2.0.0/tests/console/resolvers/route53.py +34 -0
  104. nate2_scripts-2.0.0/tests/console/resolvers/s3.py +305 -0
  105. nate2_scripts-2.0.0/tests/console/resolvers/sagemaker.py +34 -0
  106. nate2_scripts-2.0.0/tests/console/resolvers/secretsmanager.py +47 -0
  107. nate2_scripts-2.0.0/tests/console/resolvers/sns.py +21 -0
  108. nate2_scripts-2.0.0/tests/console/resolvers/sqs.py +21 -0
  109. nate2_scripts-2.0.0/tests/console/resolvers/ssm.py +89 -0
  110. nate2_scripts-2.0.0/tests/console/resolvers/stepfunctions.py +21 -0
  111. nate2_scripts-2.0.0/tests/console/resolvers/storagegateway.py +34 -0
  112. nate2_scripts-2.0.0/tests/console/resolvers/support.py +36 -0
  113. nate2_scripts-2.0.0/tests/console/resolvers/vpc.py +58 -0
  114. nate2_scripts-2.0.0/tests/console/test_aws/config +35 -0
  115. nate2_scripts-2.0.0/tests/console/test_aws/credentials +11 -0
  116. nate2_scripts-1.0.3/README.md +0 -93
  117. nate2_scripts-1.0.3/src/nate2_scripts/console/__init__.py +0 -211
  118. nate2_scripts-1.0.3/src/nate2_scripts/console/arnResolver.py +0 -514
  119. nate2_scripts-1.0.3/src/nate2_scripts/console/destitationResolver.py +0 -116
  120. nate2_scripts-1.0.3/src/nate2_scripts/console/nameResolver.py +0 -106
  121. nate2_scripts-1.0.3/src/nate2_scripts/console/regionCache.py +0 -50
  122. nate2_scripts-1.0.3/src/nate2_scripts/console/resourceIdResolver.py +0 -176
  123. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/.github/workflows/publish.yml +0 -0
  124. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/CONSOLE-NOTES.md +0 -0
  125. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/LICENSE +0 -0
  126. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/src/nate2_scripts/__init__.py +0 -0
  127. {nate2_scripts-1.0.3 → nate2_scripts-2.0.0}/src/nate2_scripts/bing_wrapper/autocomplete.sh +0 -0
@@ -1,3 +1,4 @@
1
1
  __pycache__
2
2
  dist
3
- .vscode
3
+ .vscode
4
+ uv.lock
@@ -1,9 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nate2-scripts
3
- Version: 1.0.3
3
+ Version: 2.0.0
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
7
7
  Requires-Python: >=3.11
8
8
  Requires-Dist: argcomplete
9
9
  Requires-Dist: boto3
10
+ Provides-Extra: dev
11
+ Requires-Dist: boto3-stubs[essential]; extra == 'dev'
12
+ Requires-Dist: moto[all]; extra == 'dev'
13
+ Requires-Dist: pytest; extra == 'dev'
@@ -0,0 +1,204 @@
1
+ [![PyPI version](https://img.shields.io/pypi/v/nate2-scripts)](https://pypi.org/project/nate2-scripts/)
2
+ [![License](https://img.shields.io/github/license/tennex-nwestfall/nate2-scripts)](https://github.com/tennex-nwestfall/nate2-scripts/blob/main/LICENSE)
3
+
4
+ # Nate2's Scripts
5
+
6
+ Utility scripts for AWS development on macOS.
7
+
8
+ To install run:
9
+
10
+ ```bash
11
+ pip install nate2-scripts
12
+ ```
13
+
14
+ To update run:
15
+
16
+ ```bash
17
+ pip install --upgrade nate2-scripts
18
+ ```
19
+
20
+ ## Scripts
21
+
22
+ ### `console`
23
+
24
+ Opens the AWS Management Console in your browser for a specific service, ARN, resource id, or resource name, using federated sign-in from your current AWS credentials. Supports multi-session via searching browser history sqlite databases.
25
+
26
+ **Requirements:**
27
+
28
+ - `AWS_PROFILE` set (e.g. via `asp`)
29
+ - `git config user.email` set — it is used as the `role_session_name` for every assume-role profile
30
+
31
+ **Example Alias:**
32
+
33
+ ```bash
34
+ # noglob is so that one can paste in logs arns that end with :*
35
+ # otherwise zsh tries to see the log arn as a glob and fails
36
+ alias co="noglob console"
37
+ # needed for autocomplete
38
+ eval "$(register-python-argcomplete console)"
39
+ ```
40
+
41
+ **Usage:**
42
+
43
+ ```bash
44
+ console [service | arn | id | name] [search ...] [-r REGION] [-f]
45
+ ```
46
+
47
+ ```bash
48
+ # change to an account/role
49
+ asp my-aws-role
50
+
51
+ # open main page of the role/account
52
+ console
53
+ # open ec2 page of the role/account
54
+ console ec2
55
+ # open lambda page of the role/account in us-west-2
56
+ console lambda -r us-west-2
57
+ # open a service page filtered by search terms (multiple words are joined)
58
+ console lambda my-function
59
+ console logs /aws/lambda/my
60
+ console cf my stack
61
+ # open instance id page of the role/account in
62
+ # whatever region that instance is in
63
+ console i-0123456789abcdef
64
+ # open an S3 bucket, prefix, or object
65
+ console s3://my-bucket/some/prefix/
66
+ # open a resource by name (log group, S3 bucket, EC2 Name tag,
67
+ # CloudFormation stack, or Lambda function)
68
+ console my-function
69
+ # open the console page for the resource the ARN points to
70
+ console arn:aws:ec2:us-east-1:012345678967:instance/i-0123456789abcdef
71
+ # force a new federated sign-in (resets the session cache)
72
+ console -f
73
+ ```
74
+
75
+ **Options:**
76
+
77
+ | Flag | Description |
78
+ | ----------------------- | ----------------------------------------------------------------- |
79
+ | `-r`, `--region <name>` | Region to open the console in; also added to the searched regions |
80
+ | `-f`, `--force` | Reset the session cache and force a new federated sign-in |
81
+
82
+ **Resolution order:**
83
+
84
+ 1. **ARN** — if the first argument parses as an ARN
85
+ 2. **Service** — if the first argument is a known service name or alias
86
+ 3. **Resource id** — if the first argument matches a known id pattern
87
+ 4. **Resource name** — looked up in parallel across all name-resolving services; the first match in priority order wins: CloudWatch log group → S3 bucket → EC2 `Name` tag → CloudFormation stack → Lambda function
88
+
89
+ **Supported services** (aliases after the `/`):
90
+
91
+ | Service | Names | Search terms |
92
+ | ---------------------- | ----------------------------------------- | ------------ |
93
+ | API Gateway | `apigateway` / `api` | |
94
+ | Artifact | `artifact` | |
95
+ | Athena | `athena` | |
96
+ | Batch | `batch` / `job` / `jobs` | |
97
+ | Bedrock | `bedrock` / `br` | |
98
+ | CloudFormation | `cloudformation` / `cf` | ✓ |
99
+ | CloudFront | `cloudfront` / `front` | |
100
+ | CloudHSM | `cloudhsm` / `hsm` | |
101
+ | CloudTrail | `cloudtrail` / `ct` | |
102
+ | CloudWatch | `cloudwatch` / `cw` | |
103
+ | CloudWatch Logs | `logs` | ✓ |
104
+ | Cognito | `cognito` / `cn` | |
105
+ | Config | `config` / `cfg` | |
106
+ | Control Tower | `controltower` | |
107
+ | Cost Management | `cost` | |
108
+ | DynamoDB | `dynamodb` / `dynamo` / `dyn` | |
109
+ | EC2 instances | `ec2` | ✓ |
110
+ | EC2 AMIs (owned by me) | `ami` | ✓ |
111
+ | ECR | `ecr` / `docker` | |
112
+ | ECS | `ecs` / `cluster` / `task` / `tasks` | |
113
+ | EFS | `efs` | |
114
+ | EKS | `eks` | |
115
+ | Elastic Beanstalk | `elasticbeanstalk` / `bean` | |
116
+ | Elastic Load Balancing | `elb` / `alb` | |
117
+ | EventBridge | `eventbridge` / `eb` | |
118
+ | FSx | `fsx` | |
119
+ | Glue | `glue` | |
120
+ | GuardDuty | `guardduty` / `guard` | |
121
+ | IAM | `iam` | |
122
+ | Inspector | `inspector` | |
123
+ | KMS | `kms` | |
124
+ | Lambda | `lambda` | ✓ |
125
+ | Lex | `lex` | |
126
+ | Lightsail | `lightsail` / `ls` | |
127
+ | Marketplace | `marketplace` / `market` / `tennex` | ✓ |
128
+ | Polly | `polly` | |
129
+ | RDS | `rds` | |
130
+ | Route 53 | `route53` / `53` | |
131
+ | S3 | `s3` | |
132
+ | SageMaker | `sagemaker` / `sage` | |
133
+ | Secrets Manager | `secretsmanager` / `secretmanager` / `sm` | |
134
+ | Security groups | `sg` | ✓ |
135
+ | SNS | `sns` | |
136
+ | SQS | `sqs` | |
137
+ | Step Functions | `step` | |
138
+ | Storage Gateway | `storagegateway` / `sgw` | |
139
+ | Support | `support` | |
140
+ | Systems Manager | `ssm` | |
141
+ | SSM Parameter Store | `parameter` / `ps` | ✓ |
142
+ | VPC | `vpc` | |
143
+ | VPC subnets | `subnet` | |
144
+ | VPC network ACLs | `acl` / `nacl` | |
145
+ | VPC Elastic IPs | `elasticip` / `eni` | |
146
+ | VPC route tables | `routetable` / `routes` | |
147
+ | VPC internet gateways | `internetgateway` / `internet` / `igw` | |
148
+ | VPC NAT gateways | `nat` | |
149
+ | VPC endpoints | `endpoints` / `end` | |
150
+ | VPC transit gateways | `transitgateway` / `transit` / `tgw` | |
151
+
152
+ `tennex` opens a Marketplace search for "tennex". Cost Management and Marketplace always open in `us-east-1`.
153
+
154
+ **Supported resource ids:** `i-*` (EC2 instance), `sg-*` (security group), `ami-*`, `lt-*` (launch template), `vol-*` (EBS volume), `snap-*` (EBS snapshot), `eni-*` (network interface), `s3://bucket[/key]`
155
+
156
+ **Supported ARNs:** `ec2` (instance), `lambda` (function), `logs` (log group, log stream), `s3` (bucket, prefix, object)
157
+
158
+ **Cross-account ARNs:** if an ARN's account differs from the current one, `console` switches to the first profile in your AWS config whose `role_arn` is in that account.
159
+
160
+ **Regions:** ids and names are searched across `us-east-1`, `us-east-2`, `us-west-1`, and `us-west-2`, plus the region passed with `-r`.
161
+
162
+ **Session cache:** federated session hashes are cached per profile for 59 minutes in `$XDG_STATE_HOME/console-sessions.json` (default `~/.local/state/console-sessions.json`). Use `-f` to reset it.
163
+
164
+ ---
165
+
166
+ ### `bing-wrapper`
167
+
168
+ A PTY wrapper that runs any command in a pseudo-terminal and plays a macOS system sound when a configurable output pattern is detected, and/or when the command finishes. Useful for long-running CLI tools (e.g. CDK deployments) that prompt for confirmation or take a while to complete.
169
+
170
+ **Requirements:** None
171
+
172
+ **Example Alias Usage:**
173
+
174
+ ```bash
175
+ # wraps cdk to play Frog.aiff when done and play Glass.aiff (default) on "(y/n)" prompts
176
+ alias cdk="bing-wrapper --end --pattern \"(y/n)\" --sound-end /System/Library/Sounds/Frog.aiff -- cdk"
177
+
178
+ # wraps nextflow to play a sound when done
179
+ alias nextflow="bing-wrapper --end -- nextflow"
180
+
181
+ # needed for autocomplete of bing-wrapper and the inner command
182
+ eval "$(bing-wrapper --autocomplete)"
183
+ ```
184
+
185
+ **Options:**
186
+
187
+ | Flag | Description | Default |
188
+ | ------------------------ | --------------------------------------------------------- | ------------ |
189
+ | `--end` | Play a sound when the command exits | off |
190
+ | `--pattern <text>` | Play a sound when `<text>` appears in output (repeatable) | none |
191
+ | `--sound-end <path>` | Sound file to play on exit | `Glass.aiff` |
192
+ | `--sound-pattern <path>` | Sound file to play on pattern match | `Glass.aiff` |
193
+ | `--sound-while <path>` | Sound file to play on loop while the command runs | none |
194
+
195
+ ---
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ pip install -e ".[dev]"
201
+ pytest
202
+ ```
203
+
204
+ Resolver tests live in `tests/console/resolvers/`, one module per service, and use `moto` to mock AWS.
@@ -0,0 +1,173 @@
1
+ # Refactor console resolvers into per-service `Resolver` classes
2
+
3
+ ## Context
4
+
5
+ The `console` command (entry point `nate2_scripts.console:main`) turns a user argument
6
+ into an AWS console URL and opens it. Today the resolution logic lives in **three
7
+ monolithic resolvers** plus a static service map, each of which handles *every* AWS
8
+ service internally via large `match`/`if` ladders:
9
+
10
+ - [destitationResolver.py](src/nate2_scripts/console/destitationResolver.py) — orchestrator + `_get_services()` static keyword→URL map
11
+ - [arnResolver.py](src/nate2_scripts/console/arnResolver.py) — one class, ~15 `parse_*` methods for every ARN service
12
+ - [nameResolver.py](src/nate2_scripts/console/nameResolver.py) — one class, parallel cross-service/region name lookup
13
+ - [resourceIdResolver.py](src/nate2_scripts/console/resourceIdResolver.py) — one class, `match` over every id prefix
14
+
15
+ The new target shape already exists in [resolvers/s3.py](src/nate2_scripts/console/resolvers/s3.py):
16
+ a `Resolver` ABC ([resolvers/__init__.py](src/nate2_scripts/console/resolvers/__init__.py))
17
+ with four dimensions — `try_resolve_name`, `try_resolve_arn`, `try_resolve_service`,
18
+ `try_resolve_id` — and **one class per service** (`S3Resolver`) implementing all four,
19
+ each returning `None` when the input isn't theirs.
20
+
21
+ Goal: move all existing resolution logic into per-service `Resolver` subclasses following
22
+ the `S3Resolver` pattern, wire a dispatcher that iterates them, and delete the three old
23
+ monolith files. **Functionality stays the same** (same URLs, same parallel name search,
24
+ same priority tie-breaking).
25
+
26
+ ## Key design points (settled)
27
+
28
+ - **Granularity:** one class per service, splitting the ec2 namespace into a separate
29
+ `Ec2Resolver` (instances, AMIs, volumes, snapshots, launch templates, ENIs, security
30
+ groups) and `VpcResolver` (vpc, subnet, route table, nat gateway, acl).
31
+ - **Static-only services** (cost, support, athena, ecr, bedrock, config, controltower,
32
+ cognito, cloudfront, route53, kms-link, storagegateway, fsx, ssm, parameter, cloudtrail,
33
+ tennex): each gets its **own** `Resolver` subclass. **No shared mixin/base** — every class
34
+ implements all four methods explicitly, with the three unused ones written out as
35
+ `return None`. `sns`, `sqs`, `ecs`, `eks` (and every other service) stay their own separate
36
+ classes; none are combined.
37
+ - **Name lookup stays parallel + priority-ordered:** logs > s3 > ec2 > cloudformation >
38
+ lambda (existing order in [nameResolver.py:24](src/nate2_scripts/console/nameResolver.py#L24)).
39
+ - **URL wrapping:** resolvers return a **bare host+path** (e.g. `console.aws.amazon.com/s3/...`
40
+ for global, `{region}.console.aws.amazon.com/...` for regional), exactly as `S3Resolver`
41
+ does. The **dispatcher** prepends `https://{mdn}`. `mdn` (multisession domain name) stays
42
+ out of `Context` — it's the dispatcher's concern.
43
+
44
+ ## Context object
45
+
46
+ Extend `Context` in [resolvers/__init__.py](src/nate2_scripts/console/resolvers/__init__.py)
47
+ so resolvers have what the monoliths used to pull from constructor args:
48
+
49
+ ```python
50
+ class Context:
51
+ session: boto3.Session # built from assumed creds in main()
52
+ default_region: str
53
+ regions: list[str] # NEW — from RegionCache, for multi-region name/id search
54
+ current_account: str # for arn.account access checks
55
+ profile: str
56
+ ```
57
+
58
+ `session` replaces the bare `boto3.client(...)` calls in the old resolvers — every
59
+ resolver uses `context.session.client(svc, region_name=region)`. In
60
+ [console/__init__.py](src/nate2_scripts/console/__init__.py) `main()`, build the session
61
+ from the `creds` already obtained via `get_credentials()` and populate `Context`.
62
+
63
+ ## New files (under `resolvers/`)
64
+
65
+ Resource-resolving services (port the matching `parse_*` / `_resolve_*` bodies verbatim,
66
+ swapping `boto3.client` → `context.session.client` and dropping the `https://{mdn}` prefix
67
+ from returned strings so the dispatcher adds it):
68
+
69
+ | File | Class | Dimensions ported from |
70
+ |---|---|---|
71
+ | `ec2.py` | `Ec2Resolver` | arn `instance`; ids `i-,sg-,ami-,lt-,vol-,snap-,eni-`; name tag:Name; service `ec2`,`sg`,`ami`,`elb`* |
72
+ | `vpc.py` | `VpcResolver` | arn `subnet`,`natgateway`; ids `vpc-,subnet-,rtb-,nat-`; service `vpc`,`subnet`,`acl` |
73
+ | `iam.py` | `IamResolver` | arn `role`,`user`; ids `role/`,`usr/`; service `iam` (global, us-east-1) |
74
+ | `lambda.py` | `LambdaResolver` | arn `function`; name function; service `lambda` |
75
+ | `logs.py` | `LogsResolver` | arn `log-group`; name log-group; service `logs`,`cw`,`cloudwatch` |
76
+ | `cloudformation.py` | `CloudFormationResolver` | name stack; service `cf`,`cloudformation` |
77
+ | `rds.py` | `RdsResolver` | arn `db`,`cluster`; service `rds` |
78
+ | `secretsmanager.py` | `SecretsManagerResolver` | arn `secret`; service `sm`,`secretsmanager`,`secretmanager` |
79
+ | `ecs.py` | `EcsResolver` | arn `cluster`,`service`,`task`,`task-definition`; service `ecs` |
80
+ | `eks.py` | `EksResolver` | arn `cluster`; service `eks` |
81
+ | `sns.py` | `SnsResolver` | arn topic; service `sns` |
82
+ | `sqs.py` | `SqsResolver` | arn queue; service `sqs` |
83
+ | `batch.py` | `BatchResolver` | arn `job-queue`,`compute-environment`,`job-definition`,`job`; service `batch` |
84
+ | `dynamodb.py` | `DynamoDbResolver` | arn `table`; service `dynamo` |
85
+ | `stepfunctions.py` | `StepFunctionsResolver` | arn `stateMachine`,`execution`; service `step` |
86
+ | `elb.py` | `ElbResolver` | arn `loadbalancer`,`targetgroup`; service `elb` |
87
+
88
+ *`elb`/`acl`/`ami`/`sg` service keywords are static list-view links — keep them on the
89
+ resolver that owns that resource where natural, or in a static resolver; either is fine.
90
+
91
+ Static-only resolvers (one class each): `cost.py`, `support.py`, `athena.py`, `ecr.py`,
92
+ `bedrock.py`, `config.py`, `controltower.py`, `cognito.py`, `cloudfront.py`, `route53.py`,
93
+ `kms.py`, `storagegateway.py`, `fsx.py`, `ssm.py`, `cloudtrail.py`, `tennex.py` — seeded
94
+ from the remaining entries of `_get_services()` in
95
+ [destitationResolver.py:60](src/nate2_scripts/console/destitationResolver.py#L60).
96
+ **No shared base/mixin.** Each class implements `try_resolve_service` (matching its own
97
+ keyword aliases) and writes out `try_resolve_name`, `try_resolve_arn`, `try_resolve_id` as
98
+ explicit `return None`.
99
+
100
+ ## Dispatcher
101
+
102
+ Rewrite [destitationResolver.py](src/nate2_scripts/console/destitationResolver.py)
103
+ `parse_destination` (keep `DestinationResolver` name + `get_service_list()` so
104
+ [console/__init__.py](src/nate2_scripts/console/__init__.py) callers are untouched) to hold
105
+ an ordered `list[Resolver]` and dispatch:
106
+
107
+ 1. Empty arg → `https://{mdn}{default_region}.console.aws.amazon.com/`.
108
+ 2. `Arn.try_parse(arg)` succeeds → **ARN input is terminal.** Try each resolver's
109
+ `try_resolve_arn`; return the first non-None. If none resolve it, print an
110
+ ARN-specific error to stderr and `sys.exit(1)` — **do not** fall through to the
111
+ service/id/name steps below.
112
+ 3. `try_resolve_service(ctx, arg, search)` across resolvers → first non-None.
113
+ 4. Else `try_resolve_id(ctx, arg)` across resolvers → first non-None.
114
+ 5. Else **name** search: run each name-capable resolver's `try_resolve_name` concurrently
115
+ (`ThreadPoolExecutor`, reuse `MAX_THREADS`), collect by each resolver's priority index,
116
+ return the lowest-priority winner — preserving [nameResolver.py:33-48](src/nate2_scripts/console/nameResolver.py#L33-L48).
117
+ Each regional resolver loops `context.regions` internally.
118
+ 6. None matched → same stderr error + `sys.exit(1)`.
119
+
120
+ Every returned bare path is wrapped as `https://{mdn}{path}` before returning.
121
+ `get_service_list()` returns the union of every resolver's known service keywords (for
122
+ argcomplete in [console/__init__.py:129](src/nate2_scripts/console/__init__.py#L129)).
123
+
124
+ - **`search` is a real second positional CLI arg.** Add a `search` positional (`nargs="?"`,
125
+ default `""`) to `parse_args` in
126
+ [console/__init__.py:115](src/nate2_scripts/console/__init__.py#L115), pass it into
127
+ `parse_destination(service, region, search)`, and thread it through to
128
+ `try_resolve_service` and `try_resolve_name`. Usage becomes `console <service> <search>`
129
+ (e.g. `console ec2 <filter>`).
130
+ - **Apply `search` to the URL when given and the target console supports it.** When
131
+ `search` is non-empty and the resolver's console view accepts a filter/search query
132
+ param, the resolver appends it (URL-encoded) so the console opens pre-filtered — e.g.
133
+ EC2 instances `...#Instances:search=<term>`, Lambda `...#/functions?fo=and&k0=...&v0=<term>`,
134
+ CloudWatch log-groups list, S3 object/bucket search, etc. Where a console view has no such
135
+ param (as noted in the `S3Resolver` comments), the resolver ignores `search` and returns
136
+ the plain URL. Each ported resolver decides per-view; an empty `search` always yields the
137
+ current (unfiltered) URL, preserving today's behavior. The exact query-param syntax per
138
+ service is verified against the live console during implementation.
139
+ - `try_resolve_*` methods **return `None`** on any failure (matching `S3Resolver`), rather
140
+ than `sys.exit` mid-resolve as the old monoliths did. Net effect: the final "not found"
141
+ error is emitted once by the dispatcher. This drops a few service-specific error strings
142
+ (e.g. "current account X cannot access Y") in favor of the unified message — a deliberate
143
+ simplification consistent with the target pattern.
144
+
145
+ ## Cleanup
146
+
147
+ Delete [arnResolver.py](src/nate2_scripts/console/arnResolver.py),
148
+ [nameResolver.py](src/nate2_scripts/console/nameResolver.py),
149
+ [resourceIdResolver.py](src/nate2_scripts/console/resourceIdResolver.py) once their logic
150
+ is ported. Move the `split()` helper from arnResolver into `resolvers/__init__.py` (shared
151
+ by several resolvers). Keep `Arn` in `resolvers/__init__.py` as the single ARN parser
152
+ (replaces `ParsedArn` + `split`-based parsing).
153
+
154
+ ## Verification
155
+
156
+ No test suite exists, so verify by running the CLI against a real profile:
157
+
158
+ 1. `asp <profile>` then reinstall/link: `pip install -e .` (or `uv pip install -e .`).
159
+ 2. Exercise each dimension and confirm the same URL opens as before the refactor:
160
+ - **service keyword:** `console s3`, `console ec2`, `console cost`, `console logs`, `console sm`
161
+ - **arn:** `console arn:aws:s3:::<bucket>`, `console arn:aws:lambda:us-east-1:<acct>:function:<fn>`,
162
+ `console arn:aws:iam::<acct>:role/<role>`
163
+ - **resource id:** `console i-<id>`, `console vpc-<id>`, `console subnet-<id>`
164
+ - **name:** `console <bucket-name>`, `console <stack-name>`, `console <lambda-name>`,
165
+ `console <ec2-Name-tag>` — confirm priority tie-break still favors logs > s3 > ec2 > cfn > lambda.
166
+ - **search arg:** `console ec2 <term>` — confirm the second positional is accepted and
167
+ threaded through (no crash; ignored by resolvers that don't use it yet).
168
+ - **empty:** `console` → region landing page.
169
+ - **unknown:** `console zzzzz` → stderr "Unknown service" + exit 1.
170
+ 3. Confirm argcomplete still lists services: `console <TAB>` (or inspect `get_service_list()`).
171
+ 4. Spot-check that the multisession domain prefix (`{mdn}`) still appears in generated URLs
172
+ when a multi-session hash is cached (the `-f` path vs cached path in
173
+ [console/__init__.py:174-185](src/nate2_scripts/console/__init__.py#L174-L185)).
@@ -0,0 +1,16 @@
1
+ # plan
2
+
3
+ I want:
4
+
5
+ - resolve arns
6
+ - tries to switch to account if -s or --switch is specified
7
+ - resolve resource names in
8
+ - resolve service with search parameter
9
+ search parameter is the second arg
10
+ - resolve ids
11
+
12
+ - p or --profile
13
+ - takes a profile glob
14
+
15
+ - f or --force
16
+ - refreshes mdn cache
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nate2-scripts"
7
- version = "1.0.3"
7
+ version = "2.0.0"
8
8
  description = "Utility scripts for AWS development on macOS"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -22,5 +22,18 @@ packages = ["src/nate2_scripts"]
22
22
  [tool.hatch.build.targets.wheel.force-include]
23
23
  "src/nate2_scripts/bing_wrapper/autocomplete.sh" = "nate2_scripts/bing_wrapper/autocomplete.sh"
24
24
 
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "boto3-stubs[essential]",
28
+ "moto[all]",
29
+ "pytest",
30
+ ]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ # Resolver test modules are named after the service they cover (s3.py, logs.py,
35
+ # ...) rather than test_*.py, so widen collection to any module under tests/.
36
+ python_files = ["*.py"]
37
+
25
38
  [project.urls]
26
39
  Homepage = "https://github.com/tennex-nwestfall/nate2-scripts"
@@ -1,5 +1,6 @@
1
1
  """PTY wrapper for cdk that plays a sound when (y/n) prompts appear."""
2
2
 
3
+ import argparse
3
4
  import fcntl
4
5
  import os
5
6
  import select
@@ -10,7 +11,6 @@ import sys
10
11
  import termios
11
12
  import threading
12
13
  import tty
13
- import argparse
14
14
 
15
15
  # _PATTERN = b"(y/n)"
16
16
  # _WINDOW = len(_PATTERN) * 2
@@ -73,7 +73,9 @@ def run(args: argparse.Namespace) -> None:
73
73
  return
74
74
 
75
75
  if args.sound_while:
76
- _sound_while_thread = threading.Thread(target=_play_sound_while_loop, daemon=True)
76
+ _sound_while_thread = threading.Thread(
77
+ target=_play_sound_while_loop, daemon=True
78
+ )
77
79
  _sound_while_thread.start()
78
80
 
79
81
  # Propagate terminal resizes to the child (also sends SIGWINCH to it).
@@ -0,0 +1,207 @@
1
+ # PYTHON_ARGCOMPLETE_OK
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ import urllib.parse
10
+ import webbrowser
11
+ from typing import Any
12
+
13
+ import argcomplete
14
+ import boto3
15
+ from argcomplete.completers import ChoicesCompleter
16
+
17
+ from nate2_scripts.console.boto3_session import update_role_names
18
+ from nate2_scripts.console.hash_grabber import HashGrabber
19
+ from nate2_scripts.console.resolvers import Context
20
+ from nate2_scripts.console.session_cache import SessionCache
21
+ from nate2_scripts.console.text_resolver import TextResolver
22
+
23
+ DEFAULT_REGIONS = ["us-east-1", "us-east-2", "us-west-1", "us-west-2"]
24
+
25
+
26
+ def get_signin_token(session: boto3.Session) -> str:
27
+ creds = session.get_credentials()
28
+ if creds is None:
29
+ print("Error: No credentials found for this session.", file=sys.stderr)
30
+ sys.exit(1)
31
+ frozen = creds.get_frozen_credentials()
32
+ session_json = json.dumps(
33
+ {
34
+ "sessionId": frozen.access_key,
35
+ "sessionKey": frozen.secret_key,
36
+ "sessionToken": frozen.token,
37
+ }
38
+ )
39
+ sess = urllib.parse.quote(session_json)
40
+ token_url = (
41
+ f"https://signin.aws.amazon.com/federation?Action=getSigninToken&Session={sess}"
42
+ )
43
+ result = subprocess.run(
44
+ ["curl", "-s", token_url], capture_output=True, text=True, check=True
45
+ )
46
+ try:
47
+ return json.loads(result.stdout)["SigninToken"]
48
+ except json.JSONDecodeError:
49
+ print("Error: Failed to parse JSON response. You may not have credentials.")
50
+ sys.exit(1)
51
+
52
+
53
+ def ensure_profile():
54
+ profile_name = os.environ.get("AWS_PROFILE", "")
55
+ if not profile_name:
56
+ print(
57
+ "Error: AWS_PROFILE environment variable is not set. run asp.",
58
+ file=sys.stderr,
59
+ )
60
+ sys.exit(1)
61
+
62
+
63
+ def parse_args() -> argparse.Namespace:
64
+ parser = argparse.ArgumentParser(
65
+ description="Open AWS console for a specific service."
66
+ )
67
+ svc_arg: Any = parser.add_argument(
68
+ "service",
69
+ type=str,
70
+ default="",
71
+ nargs="?",
72
+ help="The AWS service to open in the console, or an AWS ARN.",
73
+ )
74
+ svc_arg.completer = ChoicesCompleter(TextResolver.get_service_list())
75
+ parser.add_argument(
76
+ "search",
77
+ type=str,
78
+ default=[],
79
+ nargs="*",
80
+ help="Optional search/filter terms applied to service or name resolution. Multiple words are joined with spaces.",
81
+ )
82
+ regions_arg: Any = parser.add_argument(
83
+ "-r",
84
+ "--region",
85
+ type=str,
86
+ default="",
87
+ help="The AWS region to use.",
88
+ )
89
+ regions_arg.completer = argcomplete.completers.ChoicesCompleter(DEFAULT_REGIONS)
90
+
91
+ parser.add_argument(
92
+ "-f",
93
+ "--force",
94
+ action="store_true",
95
+ help="Force new federations. This will reset the federations cache.",
96
+ )
97
+ argcomplete.autocomplete(parser)
98
+ args = parser.parse_intermixed_args()
99
+ # collapse multi-word search into a single string for the resolvers
100
+ args.search = " ".join(args.search)
101
+
102
+ return args
103
+
104
+
105
+ def get_suffix(
106
+ service: str,
107
+ search: str,
108
+ region: str | None,
109
+ session: boto3.Session,
110
+ ) -> tuple[str | None, Context]:
111
+
112
+ # add region if it is not already in the default searchable regions list
113
+ regions = DEFAULT_REGIONS.copy()
114
+ if region:
115
+ session._session.set_config_variable("region", region)
116
+ if region not in regions:
117
+ regions.append(region)
118
+
119
+ # set up context
120
+ context = Context()
121
+ context.session = session
122
+ context.regions = regions
123
+
124
+ # get account data
125
+ context.identity = context.session.client("sts").get_caller_identity()
126
+
127
+ resolver = TextResolver(context)
128
+
129
+ suffix = resolver.get_destination_suffix(service, search)
130
+ return (suffix, context)
131
+
132
+
133
+ def build_signin_url(session: boto3.Session, suffix: str) -> str:
134
+ region = session.region_name
135
+ signin_token = get_signin_token(session)
136
+ dest = urllib.parse.quote(
137
+ f"https://{region}.console.aws.amazon.com{suffix}", safe=""
138
+ )
139
+ return f"https://signin.aws.amazon.com/federation?Action=login&Issuer=&SigninToken={signin_token}&Destination={dest}"
140
+
141
+
142
+ def build_regular_url(session: boto3.Session, suffix: str, mdn: str) -> str:
143
+ region = session.region_name
144
+ return f"https://{mdn}.{region}.console.aws.amazon.com{suffix}"
145
+
146
+
147
+ def open(url: str):
148
+ print(f"Opening console URL: {url}")
149
+ webbrowser.open(url)
150
+
151
+
152
+ def open_console(suffix: str, context: Context, session_cache: SessionCache):
153
+ print(f"Opening console with suffix: {suffix}")
154
+ if not context.useprefix:
155
+ open(f"https://{suffix}")
156
+ else:
157
+ hash = session_cache.get_session_hash(context.session.profile_name)
158
+ if hash is None:
159
+ print(
160
+ f"{context.session.profile_name} has no cached session. Grabbing signin token."
161
+ )
162
+ url = build_signin_url(context.session, suffix)
163
+ open(url)
164
+ hash = HashGrabber.get_hash(context.identity["Account"], time.time())
165
+ if hash is None:
166
+ print("Failed to grab hash.")
167
+ sys.exit(1)
168
+ print(
169
+ f"Successfully grabbed hash for {context.session.profile_name}: {hash}"
170
+ )
171
+ session_cache.record_valid_hash(context.session.profile_name, hash)
172
+
173
+ else:
174
+ multisession_domain_name = f"{context.identity['Account']}-{hash}"
175
+ print(
176
+ f"{context.session.profile_name} cache has cached multi session with domain: {multisession_domain_name}"
177
+ )
178
+ url = build_regular_url(context.session, suffix, multisession_domain_name)
179
+ open(url)
180
+
181
+
182
+ def main() -> None:
183
+ ensure_profile()
184
+ args = parse_args()
185
+
186
+ session = boto3.Session()
187
+ session_cache = SessionCache()
188
+ update_role_names(session)
189
+
190
+ if args.force:
191
+ print("Force passed. Resetting session cache.")
192
+ session_cache.reset()
193
+
194
+ print("Default region is: ", session.region_name)
195
+
196
+ suffix, context = get_suffix(args.service, args.search, args.region, session)
197
+ if suffix:
198
+ open_console(suffix, context, session_cache)
199
+ else:
200
+ print("Cannot open console page.")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
205
+
206
+
207
+ # set user to OrganizationAccountAccessRole/nwestfall@tennex.io