aws-bootstrap-g4dn 0.1.0__tar.gz → 0.2.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 (38) hide show
  1. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/.github/workflows/publish-to-pypi.yml +2 -0
  2. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/PKG-INFO +12 -2
  3. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/README.md +11 -1
  4. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/cli.py +34 -1
  5. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/tests/test_cli.py +110 -0
  6. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap_g4dn.egg-info/PKG-INFO +12 -2
  7. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  8. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  9. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/.github/workflows/ci.yml +0 -0
  10. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/.gitignore +0 -0
  11. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/.pre-commit-config.yaml +0 -0
  12. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/CLAUDE.md +0 -0
  13. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  14. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/CONTRIBUTING.md +0 -0
  15. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/LICENSE +0 -0
  16. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/SECURITY.md +0 -0
  17. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/__init__.py +0 -0
  18. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/config.py +0 -0
  19. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/ec2.py +0 -0
  20. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/resources/__init__.py +0 -0
  21. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/resources/gpu_benchmark.py +0 -0
  22. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/resources/gpu_smoke_test.ipynb +0 -0
  23. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/resources/remote_setup.sh +0 -0
  24. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/resources/requirements.txt +0 -0
  25. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/ssh.py +0 -0
  26. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/tests/__init__.py +0 -0
  27. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/tests/test_config.py +0 -0
  28. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/tests/test_ec2.py +0 -0
  29. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/tests/test_ssh_config.py +0 -0
  30. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap/tests/test_ssh_gpu.py +0 -0
  31. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap_g4dn.egg-info/SOURCES.txt +0 -0
  32. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap_g4dn.egg-info/dependency_links.txt +0 -0
  33. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap_g4dn.egg-info/entry_points.txt +0 -0
  34. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap_g4dn.egg-info/requires.txt +0 -0
  35. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/aws_bootstrap_g4dn.egg-info/top_level.txt +0 -0
  36. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/pyproject.toml +0 -0
  37. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/setup.cfg +0 -0
  38. {aws_bootstrap_g4dn-0.1.0 → aws_bootstrap_g4dn-0.2.0}/uv.lock +0 -0
@@ -7,6 +7,8 @@ jobs:
7
7
  build:
8
8
  name: Build distribution
9
9
  runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
10
12
 
11
13
  steps:
12
14
  - uses: actions/checkout@v4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-bootstrap-g4dn
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Bootstrap AWS EC2 GPU instances for hybrid local-remote development
5
5
  Author: Adam Ever-Hadani
6
6
  License-Expression: MIT
@@ -66,6 +66,16 @@ ssh aws-gpu1 # You're in, venv activated, PyTorch works
66
66
  pip install aws-bootstrap-g4dn
67
67
  ```
68
68
 
69
+ ### With uvx (no install needed)
70
+
71
+ [uvx](https://docs.astral.sh/uv/guides/tools/) runs the CLI directly in a temporary environment — no global install required:
72
+
73
+ ```bash
74
+ uvx --from aws-bootstrap-g4dn aws-bootstrap launch
75
+ uvx --from aws-bootstrap-g4dn aws-bootstrap status
76
+ uvx --from aws-bootstrap-g4dn aws-bootstrap terminate
77
+ ```
78
+
69
79
  ### From source (development)
70
80
 
71
81
  ```bash
@@ -75,7 +85,7 @@ uv venv
75
85
  uv sync
76
86
  ```
77
87
 
78
- Both methods install the `aws-bootstrap` CLI.
88
+ All methods install the `aws-bootstrap` CLI.
79
89
 
80
90
  ## SSH Key Setup
81
91
 
@@ -50,6 +50,16 @@ ssh aws-gpu1 # You're in, venv activated, PyTorch works
50
50
  pip install aws-bootstrap-g4dn
51
51
  ```
52
52
 
53
+ ### With uvx (no install needed)
54
+
55
+ [uvx](https://docs.astral.sh/uv/guides/tools/) runs the CLI directly in a temporary environment — no global install required:
56
+
57
+ ```bash
58
+ uvx --from aws-bootstrap-g4dn aws-bootstrap launch
59
+ uvx --from aws-bootstrap-g4dn aws-bootstrap status
60
+ uvx --from aws-bootstrap-g4dn aws-bootstrap terminate
61
+ ```
62
+
53
63
  ### From source (development)
54
64
 
55
65
  ```bash
@@ -59,7 +69,7 @@ uv venv
59
69
  uv sync
60
70
  ```
61
71
 
62
- Both methods install the `aws-bootstrap` CLI.
72
+ All methods install the `aws-bootstrap` CLI.
63
73
 
64
74
  ## SSH Key Setup
65
75
 
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
5
5
  from pathlib import Path
6
6
 
7
7
  import boto3
8
+ import botocore.exceptions
8
9
  import click
9
10
 
10
11
  from .config import LaunchConfig
@@ -56,7 +57,39 @@ def warn(msg: str) -> None:
56
57
  click.secho(f" WARNING: {msg}", fg="yellow", err=True)
57
58
 
58
59
 
59
- @click.group()
60
+ class _AWSGroup(click.Group):
61
+ """Click group that catches common AWS credential/auth errors."""
62
+
63
+ def invoke(self, ctx):
64
+ try:
65
+ return super().invoke(ctx)
66
+ except botocore.exceptions.NoCredentialsError:
67
+ raise CLIError(
68
+ "Unable to locate AWS credentials.\n\n"
69
+ " Make sure you have configured AWS credentials using one of:\n"
70
+ " - Set the AWS_PROFILE environment variable: export AWS_PROFILE=<profile-name>\n"
71
+ " - Pass --profile to the command: aws-bootstrap <command> --profile <profile-name>\n"
72
+ " - Configure a default profile: aws configure\n\n"
73
+ " See: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
74
+ ) from None
75
+ except botocore.exceptions.ProfileNotFound as e:
76
+ raise CLIError(f"{e}\n\n List available profiles with: aws configure list-profiles") from None
77
+ except botocore.exceptions.PartialCredentialsError as e:
78
+ raise CLIError(
79
+ f"Incomplete AWS credentials: {e}\n\n Check your AWS configuration with: aws configure list"
80
+ ) from None
81
+ except botocore.exceptions.ClientError as e:
82
+ code = e.response["Error"]["Code"]
83
+ if code in ("AuthFailure", "UnauthorizedOperation", "ExpiredTokenException", "ExpiredToken"):
84
+ raise CLIError(
85
+ f"AWS authorization failed: {e.response['Error']['Message']}\n\n"
86
+ " Your credentials may be expired or lack the required permissions.\n"
87
+ " Check your AWS configuration with: aws configure list"
88
+ ) from None
89
+ raise
90
+
91
+
92
+ @click.group(cls=_AWSGroup)
60
93
  @click.version_option(package_name="aws-bootstrap-g4dn")
61
94
  def main():
62
95
  """Bootstrap AWS EC2 GPU instances for hybrid local-remote development."""
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
5
5
  from pathlib import Path
6
6
  from unittest.mock import patch
7
7
 
8
+ import botocore.exceptions
8
9
  from click.testing import CliRunner
9
10
 
10
11
  from aws_bootstrap.cli import main
@@ -526,3 +527,112 @@ def test_status_without_gpu_flag_no_ssh(mock_find, mock_spot, mock_session, mock
526
527
  assert result.exit_code == 0
527
528
  mock_gpu.assert_not_called()
528
529
  mock_details.assert_not_called()
530
+
531
+
532
+ # ---------------------------------------------------------------------------
533
+ # AWS credential / auth error handling tests
534
+ # ---------------------------------------------------------------------------
535
+
536
+
537
+ @patch("aws_bootstrap.cli.find_tagged_instances")
538
+ @patch("aws_bootstrap.cli.boto3.Session")
539
+ def test_no_credentials_shows_friendly_error(mock_session, mock_find):
540
+ """NoCredentialsError should show a helpful message, not a raw traceback."""
541
+ mock_find.side_effect = botocore.exceptions.NoCredentialsError()
542
+ runner = CliRunner()
543
+ result = runner.invoke(main, ["status"])
544
+ assert result.exit_code != 0
545
+ assert "Unable to locate AWS credentials" in result.output
546
+ assert "AWS_PROFILE" in result.output
547
+ assert "--profile" in result.output
548
+ assert "aws configure" in result.output
549
+
550
+
551
+ @patch("aws_bootstrap.cli.boto3.Session")
552
+ def test_profile_not_found_shows_friendly_error(mock_session):
553
+ """ProfileNotFound should show the missing profile name and list command."""
554
+ mock_session.side_effect = botocore.exceptions.ProfileNotFound(profile="nonexistent")
555
+ runner = CliRunner()
556
+ result = runner.invoke(main, ["status", "--profile", "nonexistent"])
557
+ assert result.exit_code != 0
558
+ assert "nonexistent" in result.output
559
+ assert "aws configure list-profiles" in result.output
560
+
561
+
562
+ @patch("aws_bootstrap.cli.find_tagged_instances")
563
+ @patch("aws_bootstrap.cli.boto3.Session")
564
+ def test_partial_credentials_shows_friendly_error(mock_session, mock_find):
565
+ """PartialCredentialsError should mention incomplete credentials."""
566
+ mock_find.side_effect = botocore.exceptions.PartialCredentialsError(
567
+ provider="env", cred_var="AWS_SECRET_ACCESS_KEY"
568
+ )
569
+ runner = CliRunner()
570
+ result = runner.invoke(main, ["status"])
571
+ assert result.exit_code != 0
572
+ assert "Incomplete AWS credentials" in result.output
573
+ assert "aws configure list" in result.output
574
+
575
+
576
+ @patch("aws_bootstrap.cli.find_tagged_instances")
577
+ @patch("aws_bootstrap.cli.boto3.Session")
578
+ def test_expired_token_shows_friendly_error(mock_session, mock_find):
579
+ """ExpiredTokenException should show authorization failure with context."""
580
+ mock_find.side_effect = botocore.exceptions.ClientError(
581
+ {"Error": {"Code": "ExpiredTokenException", "Message": "The security token is expired"}},
582
+ "DescribeInstances",
583
+ )
584
+ runner = CliRunner()
585
+ result = runner.invoke(main, ["status"])
586
+ assert result.exit_code != 0
587
+ assert "AWS authorization failed" in result.output
588
+ assert "expired" in result.output.lower()
589
+
590
+
591
+ @patch("aws_bootstrap.cli.find_tagged_instances")
592
+ @patch("aws_bootstrap.cli.boto3.Session")
593
+ def test_auth_failure_shows_friendly_error(mock_session, mock_find):
594
+ """AuthFailure ClientError should show authorization failure message."""
595
+ mock_find.side_effect = botocore.exceptions.ClientError(
596
+ {"Error": {"Code": "AuthFailure", "Message": "credentials are invalid"}},
597
+ "DescribeInstances",
598
+ )
599
+ runner = CliRunner()
600
+ result = runner.invoke(main, ["status"])
601
+ assert result.exit_code != 0
602
+ assert "AWS authorization failed" in result.output
603
+
604
+
605
+ @patch("aws_bootstrap.cli.find_tagged_instances")
606
+ @patch("aws_bootstrap.cli.boto3.Session")
607
+ def test_unhandled_client_error_propagates(mock_session, mock_find):
608
+ """Non-auth ClientErrors should propagate without being caught."""
609
+ mock_find.side_effect = botocore.exceptions.ClientError(
610
+ {"Error": {"Code": "UnknownError", "Message": "something else"}},
611
+ "DescribeInstances",
612
+ )
613
+ runner = CliRunner()
614
+ result = runner.invoke(main, ["status"])
615
+ assert result.exit_code != 0
616
+ assert isinstance(result.exception, botocore.exceptions.ClientError)
617
+
618
+
619
+ @patch("aws_bootstrap.cli.find_tagged_instances")
620
+ @patch("aws_bootstrap.cli.boto3.Session")
621
+ def test_no_credentials_caught_on_terminate(mock_session, mock_find):
622
+ """Credential errors are caught for all subcommands, not just status."""
623
+ mock_find.side_effect = botocore.exceptions.NoCredentialsError()
624
+ runner = CliRunner()
625
+ result = runner.invoke(main, ["terminate"])
626
+ assert result.exit_code != 0
627
+ assert "Unable to locate AWS credentials" in result.output
628
+
629
+
630
+ @patch("aws_bootstrap.cli.list_instance_types")
631
+ @patch("aws_bootstrap.cli.boto3.Session")
632
+ def test_no_credentials_caught_on_list(mock_session, mock_list):
633
+ """Credential errors are caught for nested subcommands (list instance-types)."""
634
+ mock_list.side_effect = botocore.exceptions.NoCredentialsError()
635
+ runner = CliRunner()
636
+ result = runner.invoke(main, ["list", "instance-types"])
637
+ assert result.exit_code != 0
638
+ assert "Unable to locate AWS credentials" in result.output
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-bootstrap-g4dn
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Bootstrap AWS EC2 GPU instances for hybrid local-remote development
5
5
  Author: Adam Ever-Hadani
6
6
  License-Expression: MIT
@@ -66,6 +66,16 @@ ssh aws-gpu1 # You're in, venv activated, PyTorch works
66
66
  pip install aws-bootstrap-g4dn
67
67
  ```
68
68
 
69
+ ### With uvx (no install needed)
70
+
71
+ [uvx](https://docs.astral.sh/uv/guides/tools/) runs the CLI directly in a temporary environment — no global install required:
72
+
73
+ ```bash
74
+ uvx --from aws-bootstrap-g4dn aws-bootstrap launch
75
+ uvx --from aws-bootstrap-g4dn aws-bootstrap status
76
+ uvx --from aws-bootstrap-g4dn aws-bootstrap terminate
77
+ ```
78
+
69
79
  ### From source (development)
70
80
 
71
81
  ```bash
@@ -75,7 +85,7 @@ uv venv
75
85
  uv sync
76
86
  ```
77
87
 
78
- Both methods install the `aws-bootstrap` CLI.
88
+ All methods install the `aws-bootstrap` CLI.
79
89
 
80
90
  ## SSH Key Setup
81
91