nate2-scripts 1.0.1__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 (128) hide show
  1. {nate2_scripts-1.0.1 → nate2_scripts-2.0.0}/.gitignore +2 -1
  2. {nate2_scripts-1.0.1 → nate2_scripts-2.0.0}/PKG-INFO +6 -2
  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-2.0.0/pyproject.toml +39 -0
  7. nate2_scripts-1.0.1/src/nate2_scripts/bing_wrapper.py → nate2_scripts-2.0.0/src/nate2_scripts/bing_wrapper/__init__.py +44 -1
  8. nate2_scripts-2.0.0/src/nate2_scripts/bing_wrapper/autocomplete.sh +30 -0
  9. nate2_scripts-2.0.0/src/nate2_scripts/console/__init__.py +207 -0
  10. nate2_scripts-2.0.0/src/nate2_scripts/console/boto3_session.py +51 -0
  11. nate2_scripts-1.0.1/src/nate2_scripts/console/hashGrabber.py → nate2_scripts-2.0.0/src/nate2_scripts/console/hash_grabber.py +27 -12
  12. nate2_scripts-2.0.0/src/nate2_scripts/console/local_refresh.py +0 -0
  13. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/__init__.py +44 -0
  14. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/apigateway.py +11 -0
  15. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/artifact.py +11 -0
  16. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/athena.py +11 -0
  17. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/batch.py +52 -0
  18. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/bedrock.py +11 -0
  19. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudformation.py +28 -0
  20. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudfront.py +12 -0
  21. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudhsm.py +11 -0
  22. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cloudtrail.py +11 -0
  23. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cognito.py +11 -0
  24. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/config.py +11 -0
  25. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/controltower.py +11 -0
  26. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/cost.py +13 -0
  27. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/dynamo.py +27 -0
  28. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ec2.py +118 -0
  29. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ecr.py +16 -0
  30. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ecs.py +61 -0
  31. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/efs.py +11 -0
  32. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/eks.py +11 -0
  33. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/elasticbeanstalk.py +11 -0
  34. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/elb.py +35 -0
  35. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/eventbridge.py +11 -0
  36. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/fsx.py +11 -0
  37. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/glue.py +11 -0
  38. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/guardduty.py +11 -0
  39. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/iam.py +43 -0
  40. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/inspector.py +11 -0
  41. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/kms.py +11 -0
  42. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/lambda_.py +45 -0
  43. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/lex.py +11 -0
  44. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/lightsail.py +11 -0
  45. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/logs.py +81 -0
  46. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/marketplace.py +24 -0
  47. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/polly.py +11 -0
  48. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/quick.py +15 -0
  49. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/rds.py +34 -0
  50. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/route53.py +16 -0
  51. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/s3.py +66 -0
  52. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/sagemaker.py +11 -0
  53. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/secretsmanager.py +31 -0
  54. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/sns.py +29 -0
  55. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/sqs.py +28 -0
  56. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/ssm.py +18 -0
  57. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/stepfunctions.py +37 -0
  58. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/storagegateway.py +11 -0
  59. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/support.py +14 -0
  60. nate2_scripts-2.0.0/src/nate2_scripts/console/resolvers/vpc.py +93 -0
  61. nate2_scripts-1.0.1/src/nate2_scripts/console/sessionCache.py → nate2_scripts-2.0.0/src/nate2_scripts/console/session_cache.py +8 -22
  62. nate2_scripts-2.0.0/src/nate2_scripts/console/text_resolver.py +193 -0
  63. nate2_scripts-2.0.0/src/nate2_scripts/console/types.py +59 -0
  64. {nate2_scripts-1.0.1/src/nate2_scripts → nate2_scripts-2.0.0}/test.py +4 -2
  65. nate2_scripts-2.0.0/tests/__init__.py +0 -0
  66. nate2_scripts-2.0.0/tests/console/__init__.py +0 -0
  67. nate2_scripts-2.0.0/tests/console/resolvers/__init__.py +29 -0
  68. nate2_scripts-2.0.0/tests/console/resolvers/apigateway.py +34 -0
  69. nate2_scripts-2.0.0/tests/console/resolvers/artifact.py +21 -0
  70. nate2_scripts-2.0.0/tests/console/resolvers/athena.py +34 -0
  71. nate2_scripts-2.0.0/tests/console/resolvers/batch.py +50 -0
  72. nate2_scripts-2.0.0/tests/console/resolvers/bedrock.py +36 -0
  73. nate2_scripts-2.0.0/tests/console/resolvers/cloudformation.py +132 -0
  74. nate2_scripts-2.0.0/tests/console/resolvers/cloudfront.py +34 -0
  75. nate2_scripts-2.0.0/tests/console/resolvers/cloudhsm.py +34 -0
  76. nate2_scripts-2.0.0/tests/console/resolvers/cloudtrail.py +34 -0
  77. nate2_scripts-2.0.0/tests/console/resolvers/cognito.py +34 -0
  78. nate2_scripts-2.0.0/tests/console/resolvers/config.py +34 -0
  79. nate2_scripts-2.0.0/tests/console/resolvers/conftest.py +40 -0
  80. nate2_scripts-2.0.0/tests/console/resolvers/controltower.py +21 -0
  81. nate2_scripts-2.0.0/tests/console/resolvers/cost.py +22 -0
  82. nate2_scripts-2.0.0/tests/console/resolvers/dynamo.py +47 -0
  83. nate2_scripts-2.0.0/tests/console/resolvers/ec2.py +269 -0
  84. nate2_scripts-2.0.0/tests/console/resolvers/ecr.py +34 -0
  85. nate2_scripts-2.0.0/tests/console/resolvers/ecs.py +60 -0
  86. nate2_scripts-2.0.0/tests/console/resolvers/efs.py +21 -0
  87. nate2_scripts-2.0.0/tests/console/resolvers/eks.py +21 -0
  88. nate2_scripts-2.0.0/tests/console/resolvers/elasticbeanstalk.py +34 -0
  89. nate2_scripts-2.0.0/tests/console/resolvers/elb.py +34 -0
  90. nate2_scripts-2.0.0/tests/console/resolvers/eventbridge.py +34 -0
  91. nate2_scripts-2.0.0/tests/console/resolvers/fsx.py +21 -0
  92. nate2_scripts-2.0.0/tests/console/resolvers/glue.py +21 -0
  93. nate2_scripts-2.0.0/tests/console/resolvers/guardduty.py +34 -0
  94. nate2_scripts-2.0.0/tests/console/resolvers/iam.py +21 -0
  95. nate2_scripts-2.0.0/tests/console/resolvers/inspector.py +21 -0
  96. nate2_scripts-2.0.0/tests/console/resolvers/kms.py +21 -0
  97. nate2_scripts-2.0.0/tests/console/resolvers/lambda_.py +202 -0
  98. nate2_scripts-2.0.0/tests/console/resolvers/lex.py +21 -0
  99. nate2_scripts-2.0.0/tests/console/resolvers/lightsail.py +34 -0
  100. nate2_scripts-2.0.0/tests/console/resolvers/logs.py +258 -0
  101. nate2_scripts-2.0.0/tests/console/resolvers/marketplace.py +60 -0
  102. nate2_scripts-2.0.0/tests/console/resolvers/polly.py +21 -0
  103. nate2_scripts-2.0.0/tests/console/resolvers/rds.py +21 -0
  104. nate2_scripts-2.0.0/tests/console/resolvers/route53.py +34 -0
  105. nate2_scripts-2.0.0/tests/console/resolvers/s3.py +305 -0
  106. nate2_scripts-2.0.0/tests/console/resolvers/sagemaker.py +34 -0
  107. nate2_scripts-2.0.0/tests/console/resolvers/secretsmanager.py +47 -0
  108. nate2_scripts-2.0.0/tests/console/resolvers/sns.py +21 -0
  109. nate2_scripts-2.0.0/tests/console/resolvers/sqs.py +21 -0
  110. nate2_scripts-2.0.0/tests/console/resolvers/ssm.py +89 -0
  111. nate2_scripts-2.0.0/tests/console/resolvers/stepfunctions.py +21 -0
  112. nate2_scripts-2.0.0/tests/console/resolvers/storagegateway.py +34 -0
  113. nate2_scripts-2.0.0/tests/console/resolvers/support.py +36 -0
  114. nate2_scripts-2.0.0/tests/console/resolvers/vpc.py +58 -0
  115. nate2_scripts-2.0.0/tests/console/test_aws/config +35 -0
  116. nate2_scripts-2.0.0/tests/console/test_aws/credentials +11 -0
  117. nate2_scripts-1.0.1/README.md +0 -90
  118. nate2_scripts-1.0.1/pyproject.toml +0 -23
  119. nate2_scripts-1.0.1/src/nate2_scripts/console/__init__.py +0 -174
  120. nate2_scripts-1.0.1/src/nate2_scripts/console/arnResolver.py +0 -514
  121. nate2_scripts-1.0.1/src/nate2_scripts/console/destitationResolver.py +0 -113
  122. nate2_scripts-1.0.1/src/nate2_scripts/console/nameResolver.py +0 -99
  123. nate2_scripts-1.0.1/src/nate2_scripts/console/regionCache.py +0 -50
  124. nate2_scripts-1.0.1/src/nate2_scripts/console/resourceIdResolver.py +0 -174
  125. {nate2_scripts-1.0.1 → nate2_scripts-2.0.0}/.github/workflows/publish.yml +0 -0
  126. {nate2_scripts-1.0.1 → nate2_scripts-2.0.0}/CONSOLE-NOTES.md +0 -0
  127. {nate2_scripts-1.0.1 → nate2_scripts-2.0.0}/LICENSE +0 -0
  128. {nate2_scripts-1.0.1 → nate2_scripts-2.0.0}/src/nate2_scripts/__init__.py +0 -0
@@ -1,3 +1,4 @@
1
1
  __pycache__
2
2
  dist
3
- .vscode
3
+ .vscode
4
+ uv.lock
@@ -1,9 +1,13 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: nate2-scripts
3
- Version: 1.0.1
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
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nate2-scripts"
7
+ version = "2.0.0"
8
+ description = "Utility scripts for AWS development on macOS"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "argcomplete",
12
+ "boto3",
13
+ ]
14
+
15
+ [project.scripts]
16
+ console = "nate2_scripts.console:main"
17
+ bing-wrapper = "nate2_scripts.bing_wrapper:main"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/nate2_scripts"]
21
+
22
+ [tool.hatch.build.targets.wheel.force-include]
23
+ "src/nate2_scripts/bing_wrapper/autocomplete.sh" = "nate2_scripts/bing_wrapper/autocomplete.sh"
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
+
38
+ [project.urls]
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
@@ -8,8 +9,8 @@ import struct
8
9
  import subprocess
9
10
  import sys
10
11
  import termios
12
+ import threading
11
13
  import tty
12
- import argparse
13
14
 
14
15
  # _PATTERN = b"(y/n)"
15
16
  # _WINDOW = len(_PATTERN) * 2
@@ -55,6 +56,28 @@ 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(
77
+ target=_play_sound_while_loop, daemon=True
78
+ )
79
+ _sound_while_thread.start()
80
+
58
81
  # Propagate terminal resizes to the child (also sends SIGWINCH to it).
59
82
  def _sigwinch(signum: int, frame) -> None:
60
83
  try:
@@ -119,6 +142,10 @@ def run(args: argparse.Namespace) -> None:
119
142
 
120
143
  _, status = os.waitpid(pid, 0)
121
144
 
145
+ _stop_sound_while.set()
146
+ if _sound_while_thread is not None:
147
+ _sound_while_thread.join(timeout=2)
148
+
122
149
  if args.end:
123
150
  subprocess.Popen(["afplay", args.sound_end])
124
151
  if os.WIFEXITED(status):
@@ -156,9 +183,25 @@ def main() -> None:
156
183
  type=str,
157
184
  default="/System/Library/Sounds/Glass.aiff",
158
185
  )
186
+ parser.add_argument(
187
+ "--sound-while",
188
+ help="play sound while command is running",
189
+ type=str,
190
+ default="",
191
+ )
192
+ parser.add_argument(
193
+ "--autocomplete",
194
+ help="return autocomplete scripts",
195
+ action="store_true",
196
+ )
159
197
 
160
198
  args = parser.parse_args()
161
199
 
200
+ if args.autocomplete:
201
+ script = os.path.join(os.path.dirname(__file__), "autocomplete.sh")
202
+ print(open(script).read())
203
+ sys.exit(0)
204
+
162
205
  if not args.command:
163
206
  parser.print_help()
164
207
  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