gui4aws 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. gui4aws/__about__.py +25 -0
  2. gui4aws/__init__.py +11 -0
  3. gui4aws/__main__.py +6 -0
  4. gui4aws/app.py +121 -0
  5. gui4aws/cli.py +194 -0
  6. gui4aws/config.py +139 -0
  7. gui4aws/demo_resources/__init__.py +79 -0
  8. gui4aws/demo_resources/_common.py +11 -0
  9. gui4aws/demo_resources/aurora.py +89 -0
  10. gui4aws/demo_resources/backup.py +167 -0
  11. gui4aws/demo_resources/compute.py +97 -0
  12. gui4aws/demo_resources/ecs.py +113 -0
  13. gui4aws/demo_resources/iam_resources.py +77 -0
  14. gui4aws/demo_resources/kms.py +51 -0
  15. gui4aws/demo_resources/messaging.py +58 -0
  16. gui4aws/demo_resources/monitoring.py +136 -0
  17. gui4aws/demo_resources/networking.py +121 -0
  18. gui4aws/demo_resources/secrets.py +71 -0
  19. gui4aws/demo_resources/storage.py +81 -0
  20. gui4aws/execution/__init__.py +17 -0
  21. gui4aws/execution/action_cache.py +200 -0
  22. gui4aws/execution/action_history.py +141 -0
  23. gui4aws/execution/aws_cli_executor.py +208 -0
  24. gui4aws/execution/boto3_executor.py +201 -0
  25. gui4aws/execution/endpoint_config.py +52 -0
  26. gui4aws/execution/execution_mode.py +14 -0
  27. gui4aws/execution/script_generator.py +208 -0
  28. gui4aws/gui/__init__.py +5 -0
  29. gui4aws/gui/action_dialog.py +355 -0
  30. gui4aws/gui/action_form.py +106 -0
  31. gui4aws/gui/cdk_dialog.py +783 -0
  32. gui4aws/gui/confirmation_dialog.py +136 -0
  33. gui4aws/gui/detail_tree.py +66 -0
  34. gui4aws/gui/diagnostic_panel.py +415 -0
  35. gui4aws/gui/filter_bar.py +242 -0
  36. gui4aws/gui/jmespath_designer_dialog.py +658 -0
  37. gui4aws/gui/json_viewer_dialog.py +216 -0
  38. gui4aws/gui/main_panel.py +515 -0
  39. gui4aws/gui/main_window.py +1233 -0
  40. gui4aws/gui/output_panel.py +189 -0
  41. gui4aws/gui/resource_table.py +119 -0
  42. gui4aws/gui/review_dialog.py +196 -0
  43. gui4aws/gui/script_editor_panel.py +194 -0
  44. gui4aws/gui/script_viewer.py +57 -0
  45. gui4aws/gui/server_manager_mixin.py +249 -0
  46. gui4aws/gui/sidebar.py +76 -0
  47. gui4aws/gui/sql_runner_dialog.py +396 -0
  48. gui4aws/gui/status_bar.py +51 -0
  49. gui4aws/gui/terraform_dialog.py +68 -0
  50. gui4aws/gui/toolbar.py +214 -0
  51. gui4aws/gui/window_helpers.py +279 -0
  52. gui4aws/gui/worker.py +146 -0
  53. gui4aws/logging_config.py +58 -0
  54. gui4aws/models.py +300 -0
  55. gui4aws/moto_server.py +241 -0
  56. gui4aws/py.typed +0 -0
  57. gui4aws/robotocore_server.py +345 -0
  58. gui4aws/services/__init__.py +5 -0
  59. gui4aws/services/athena/__init__.py +5 -0
  60. gui4aws/services/athena/actions.py +278 -0
  61. gui4aws/services/athena/models.py +34 -0
  62. gui4aws/services/athena/service.py +69 -0
  63. gui4aws/services/athena/views.py +95 -0
  64. gui4aws/services/aurora/__init__.py +9 -0
  65. gui4aws/services/aurora/actions.py +1072 -0
  66. gui4aws/services/aurora/models.py +95 -0
  67. gui4aws/services/aurora/service.py +202 -0
  68. gui4aws/services/aurora/views.py +183 -0
  69. gui4aws/services/backup/__init__.py +13 -0
  70. gui4aws/services/backup/actions.py +601 -0
  71. gui4aws/services/backup/models.py +93 -0
  72. gui4aws/services/backup/service.py +154 -0
  73. gui4aws/services/backup/views.py +151 -0
  74. gui4aws/services/cloudformation/__init__.py +0 -0
  75. gui4aws/services/cloudformation/actions.py +285 -0
  76. gui4aws/services/cloudformation/models.py +19 -0
  77. gui4aws/services/cloudformation/service.py +92 -0
  78. gui4aws/services/cloudformation/views.py +35 -0
  79. gui4aws/services/cloudwatch/__init__.py +0 -0
  80. gui4aws/services/cloudwatch/actions.py +303 -0
  81. gui4aws/services/cloudwatch/models.py +49 -0
  82. gui4aws/services/cloudwatch/service.py +81 -0
  83. gui4aws/services/cloudwatch/views.py +99 -0
  84. gui4aws/services/ecs/__init__.py +5 -0
  85. gui4aws/services/ecs/actions.py +760 -0
  86. gui4aws/services/ecs/models.py +62 -0
  87. gui4aws/services/ecs/service.py +185 -0
  88. gui4aws/services/ecs/views.py +180 -0
  89. gui4aws/services/iam/__init__.py +0 -0
  90. gui4aws/services/iam/actions.py +368 -0
  91. gui4aws/services/iam/models.py +55 -0
  92. gui4aws/services/iam/service.py +129 -0
  93. gui4aws/services/iam/views.py +81 -0
  94. gui4aws/services/kms/__init__.py +5 -0
  95. gui4aws/services/kms/actions.py +388 -0
  96. gui4aws/services/kms/models.py +49 -0
  97. gui4aws/services/kms/service.py +87 -0
  98. gui4aws/services/kms/views.py +103 -0
  99. gui4aws/services/lambdas/__init__.py +0 -0
  100. gui4aws/services/lambdas/actions.py +243 -0
  101. gui4aws/services/lambdas/models.py +22 -0
  102. gui4aws/services/lambdas/service.py +53 -0
  103. gui4aws/services/lambdas/views.py +48 -0
  104. gui4aws/services/networking/__init__.py +5 -0
  105. gui4aws/services/networking/actions.py +545 -0
  106. gui4aws/services/networking/models.py +87 -0
  107. gui4aws/services/networking/service.py +136 -0
  108. gui4aws/services/networking/views.py +203 -0
  109. gui4aws/services/s3/__init__.py +0 -0
  110. gui4aws/services/s3/actions.py +215 -0
  111. gui4aws/services/s3/models.py +27 -0
  112. gui4aws/services/s3/service.py +72 -0
  113. gui4aws/services/s3/views.py +49 -0
  114. gui4aws/services/secrets/__init__.py +5 -0
  115. gui4aws/services/secrets/actions.py +264 -0
  116. gui4aws/services/secrets/models.py +22 -0
  117. gui4aws/services/secrets/service.py +73 -0
  118. gui4aws/services/secrets/views.py +47 -0
  119. gui4aws/services/service_registry.py +144 -0
  120. gui4aws/services/ses/__init__.py +0 -0
  121. gui4aws/services/ses/actions.py +128 -0
  122. gui4aws/services/ses/models.py +25 -0
  123. gui4aws/services/ses/service.py +46 -0
  124. gui4aws/services/ses/views.py +44 -0
  125. gui4aws/services/sns/__init__.py +0 -0
  126. gui4aws/services/sns/actions.py +183 -0
  127. gui4aws/services/sns/models.py +27 -0
  128. gui4aws/services/sns/service.py +66 -0
  129. gui4aws/services/sns/views.py +41 -0
  130. gui4aws/services/sqs/__init__.py +0 -0
  131. gui4aws/services/sqs/actions.py +243 -0
  132. gui4aws/services/sqs/models.py +18 -0
  133. gui4aws/services/sqs/service.py +59 -0
  134. gui4aws/services/sqs/views.py +40 -0
  135. gui4aws/services/ssm/__init__.py +5 -0
  136. gui4aws/services/ssm/actions.py +223 -0
  137. gui4aws/services/ssm/models.py +20 -0
  138. gui4aws/services/ssm/service.py +93 -0
  139. gui4aws/services/ssm/views.py +49 -0
  140. gui4aws/sql_runner/__init__.py +1 -0
  141. gui4aws/sql_runner/connection.py +243 -0
  142. gui4aws/testing/__init__.py +7 -0
  143. gui4aws/testing/moto_support.py +30 -0
  144. gui4aws-0.0.1.dist-info/METADATA +94 -0
  145. gui4aws-0.0.1.dist-info/RECORD +148 -0
  146. gui4aws-0.0.1.dist-info/WHEEL +4 -0
  147. gui4aws-0.0.1.dist-info/entry_points.txt +2 -0
  148. gui4aws-0.0.1.dist-info/licenses/LICENSE +21 -0
gui4aws/__about__.py ADDED
@@ -0,0 +1,25 @@
1
+ """Metadata for gui4aws."""
2
+
3
+ __all__ = [
4
+ "__credits__",
5
+ "__dependencies__",
6
+ "__description__",
7
+ "__keywords__",
8
+ "__license__",
9
+ "__readme__",
10
+ "__requires_python__",
11
+ "__status__",
12
+ "__title__",
13
+ "__version__",
14
+ ]
15
+
16
+ __title__ = "gui4aws"
17
+ __version__ = "0.0.1"
18
+ __description__ = "Tkinter GUI for AWS"
19
+ __readme__ = "README.md"
20
+ __credits__ = [{"name": "Matthew Martin", "email": "matthewdeanmartin@gmail.com"}]
21
+ __keywords__ = ["gui4aws", "aws", "tkinter", "gui"]
22
+ __license__ = "MIT"
23
+ __requires_python__ = ">=3.10"
24
+ __status__ = "3 - Alpha"
25
+ __dependencies__ = ["boto3>=1.35.0", "keyring>=25.0.0", "tomli>=2.4.1 ; python_full_version < '3.11'"]
gui4aws/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """gui4aws: A thoughtful Tkinter GUI for AWS.
2
+
3
+ This package provides a desktop application for browsing and managing AWS resources
4
+ with a focus on safety, transparency, and developer productivity. It supports
5
+ both boto3 and AWS CLI execution modes, and includes built-in support for
6
+ local emulation via Moto and Robotocore.
7
+ """
8
+
9
+ from gui4aws.__about__ import __version__
10
+
11
+ __all__ = ["__version__"]
gui4aws/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Main entry point for gui4aws when run via `python -m gui4aws`."""
2
+
3
+ from gui4aws.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
gui4aws/app.py ADDED
@@ -0,0 +1,121 @@
1
+ """AppContext: the single source of truth for runtime selections.
2
+
3
+ The GUI binds widget values to fields on AppContext and asks AppContext for the right executor.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from gui4aws.execution.action_cache import ActionCache
13
+ from gui4aws.execution.action_history import ActionHistory
14
+ from gui4aws.execution.aws_cli_executor import AwsCliExecutor
15
+ from gui4aws.execution.boto3_executor import Boto3Executor
16
+ from gui4aws.execution.endpoint_config import EndpointConfig, EndpointMode
17
+ from gui4aws.execution.execution_mode import ExecutionMode
18
+ from gui4aws.services.service_registry import ServiceRegistry, default_registry
19
+
20
+ __all__ = ["AWS_PARTITIONS", "AppContext"]
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # AWS partitions with their default region for region enumeration.
25
+ # Keys are partition names; values are a (default_region, display_label) tuple.
26
+ AWS_PARTITIONS: dict[str, tuple[str, str]] = {
27
+ "aws": ("us-east-1", "AWS Standard"),
28
+ "aws-us-gov": ("us-gov-west-1", "AWS GovCloud (US)"),
29
+ "aws-cn": ("cn-north-1", "AWS China"),
30
+ "aws-iso": ("us-iso-east-1", "AWS ISO"),
31
+ "aws-iso-b": ("us-isob-east-1", "AWS ISO-B"),
32
+ }
33
+
34
+
35
+ @dataclass
36
+ class AppContext:
37
+ """Holds profile/region/mode/endpoint plus the action history and registry."""
38
+
39
+ profile_name: str | None = None
40
+ region_name: str = "us-east-1"
41
+ partition: str = "aws"
42
+ mode: ExecutionMode = ExecutionMode.BOTO3
43
+ endpoint_config: EndpointConfig = field(default_factory=EndpointConfig)
44
+ history: ActionHistory = field(default_factory=ActionHistory)
45
+ registry: ServiceRegistry = field(default_factory=default_registry)
46
+ action_cache: ActionCache = field(default_factory=ActionCache)
47
+
48
+ def set_mode(self, mode: ExecutionMode) -> None:
49
+ """Change execution mode (AWS CLI vs boto3)."""
50
+ logger.info("execution mode -> %s", mode)
51
+ self.mode = mode
52
+ self.action_cache.clear()
53
+
54
+ def set_region(self, region_name: str) -> None:
55
+ """Change region; resource lists should refresh after this."""
56
+ logger.info("region -> %s", region_name)
57
+ self.region_name = region_name
58
+ self.action_cache.clear()
59
+
60
+ def set_partition(self, partition: str) -> None:
61
+ """Change AWS partition (aws, aws-us-gov, aws-cn, aws-iso, aws-iso-b)."""
62
+ logger.info("partition -> %s", partition)
63
+ self.partition = partition
64
+ self.action_cache.clear()
65
+
66
+ def set_profile(self, profile_name: str | None) -> None:
67
+ """Change AWS profile (None means rely on environment)."""
68
+ logger.info("profile -> %s", profile_name)
69
+ self.profile_name = profile_name
70
+ self.action_cache.clear()
71
+
72
+ def set_endpoint(self, mode: EndpointMode, endpoint_url: str | None = None) -> None:
73
+ """Change endpoint mode + URL."""
74
+ self.endpoint_config = EndpointConfig.for_mode(mode, endpoint_url)
75
+ logger.info("endpoint -> %s url=%s", mode, endpoint_url)
76
+ self.action_cache.clear()
77
+
78
+ def boto3_executor(self) -> Boto3Executor:
79
+ """Build a fresh Boto3Executor that reflects current selections."""
80
+ return Boto3Executor(
81
+ profile_name=self.profile_name,
82
+ region_name=self.region_name,
83
+ endpoint_config=self.endpoint_config,
84
+ )
85
+
86
+ def aws_cli_executor(self) -> AwsCliExecutor:
87
+ """Build a fresh AwsCliExecutor that reflects current selections."""
88
+ return AwsCliExecutor(
89
+ profile_name=self.profile_name,
90
+ region_name=self.region_name,
91
+ endpoint_config=self.endpoint_config,
92
+ )
93
+
94
+ def execute(self, action: Any, inputs: dict[str, str]) -> Any:
95
+ """Dispatch to whichever executor matches the current mode."""
96
+ cache_key = self.action_cache.build_key(
97
+ action,
98
+ inputs,
99
+ mode=self.mode,
100
+ profile_name=self.profile_name,
101
+ region_name=self.region_name,
102
+ endpoint_config=self.endpoint_config,
103
+ )
104
+ cached = self.action_cache.get(cache_key)
105
+ if cached is not None:
106
+ return cached
107
+ result: Any
108
+ if self.mode is ExecutionMode.BOTO3:
109
+ result = self.boto3_executor().execute(action, inputs)
110
+ else:
111
+ result = self.aws_cli_executor().execute(action, inputs)
112
+ if self.action_cache.should_cache(action, result):
113
+ self.action_cache.put(cache_key, result)
114
+ return result
115
+
116
+ def invalidate_read_cache(self, service_id: str | None = None) -> None:
117
+ """Drop cached read-only results globally or for one service."""
118
+ if service_id is None:
119
+ self.action_cache.clear()
120
+ return
121
+ self.action_cache.invalidate_service(service_id)
gui4aws/cli.py ADDED
@@ -0,0 +1,194 @@
1
+ """Command-line entry point for gui4aws."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import shutil
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+
12
+ import boto3
13
+ import botocore
14
+
15
+ from gui4aws.__about__ import __version__
16
+ from gui4aws.app import AppContext
17
+ from gui4aws.execution.endpoint_config import EndpointConfig, EndpointMode
18
+ from gui4aws.execution.execution_mode import ExecutionMode
19
+ from gui4aws.logging_config import configure_logging
20
+
21
+ __all__ = ["build_parser", "main"]
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def build_parser() -> argparse.ArgumentParser:
27
+ """Construct the argparse parser."""
28
+ parser = argparse.ArgumentParser(prog="gui4aws", description="Tkinter GUI for AWS")
29
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
30
+ parser.add_argument("--log-level", default="INFO", help="DEBUG, INFO, WARNING, ERROR")
31
+ parser.add_argument("--log-file", type=Path, default=None, help="optional file to also log to")
32
+
33
+ subparsers = parser.add_subparsers(dest="command")
34
+
35
+ gui_parser = subparsers.add_parser("gui", help="launch the GUI")
36
+ add_common_runtime_options(gui_parser)
37
+
38
+ doctor_parser = subparsers.add_parser("doctor", help="environment diagnostics")
39
+ doctor_parser.add_argument("--check-aws-cli", action="store_true", help="report aws CLI presence")
40
+ doctor_parser.add_argument("--check-boto3", action="store_true", help="report boto3 version")
41
+ doctor_parser.add_argument("--check-docker", action="store_true", help="report docker presence")
42
+
43
+ subparsers.add_parser("list-profiles", help="list available AWS profiles")
44
+ list_regions_parser = subparsers.add_parser("list-regions", help="list known AWS regions")
45
+ from gui4aws.app import AWS_PARTITIONS
46
+
47
+ list_regions_parser.add_argument(
48
+ "--partition",
49
+ default="aws",
50
+ choices=list(AWS_PARTITIONS.keys()),
51
+ help="AWS partition to list regions for (default: aws)",
52
+ )
53
+
54
+ return parser
55
+
56
+
57
+ def add_common_runtime_options(parser: argparse.ArgumentParser) -> None:
58
+ """Add the runtime-selection options shared by ``gui`` (and future subcommands)."""
59
+ from gui4aws.app import AWS_PARTITIONS
60
+
61
+ parser.add_argument("--profile", default=None, help="AWS profile name")
62
+ parser.add_argument("--region", default="us-east-1", help="AWS region")
63
+ parser.add_argument(
64
+ "--partition",
65
+ default="aws",
66
+ choices=list(AWS_PARTITIONS.keys()),
67
+ help="AWS partition (aws, aws-us-gov, aws-cn, aws-iso, aws-iso-b)",
68
+ )
69
+ parser.add_argument(
70
+ "--mode",
71
+ default=ExecutionMode.BOTO3.value,
72
+ choices=[m.value for m in ExecutionMode],
73
+ help="execution mode",
74
+ )
75
+ parser.add_argument(
76
+ "--endpoint-mode",
77
+ default=EndpointMode.AWS.value,
78
+ choices=[m.value for m in EndpointMode],
79
+ help="endpoint mode",
80
+ )
81
+ parser.add_argument("--endpoint-url", default=None, help="endpoint URL (required for --endpoint-mode custom)")
82
+
83
+
84
+ def main(argv: Sequence[str] | None = None) -> int:
85
+ """Entry point for the ``gui4aws`` command."""
86
+ parser = build_parser()
87
+ args = parser.parse_args(argv)
88
+ configure_logging(args.log_level, args.log_file)
89
+
90
+ if args.command in (None, "gui"):
91
+ return run_gui(args)
92
+ if args.command == "doctor":
93
+ return run_doctor(args)
94
+ if args.command == "list-profiles":
95
+ return run_list_profiles()
96
+ if args.command == "list-regions":
97
+ return run_list_regions(args)
98
+ parser.error(f"unknown command: {args.command}")
99
+ return 2
100
+
101
+
102
+ def run_gui(args: argparse.Namespace) -> int:
103
+ """Launch the GUI window."""
104
+ from gui4aws.config import load_config
105
+ from gui4aws.gui.main_window import create_main_window
106
+
107
+ saved = load_config()
108
+
109
+ # CLI args take precedence over saved config; config takes precedence over defaults.
110
+ cli_profile = getattr(args, "profile", None)
111
+ cli_region = getattr(args, "region", None)
112
+ cli_partition = getattr(args, "partition", None)
113
+
114
+ profile = cli_profile or (saved.default_profile or None)
115
+ partition = cli_partition or saved.default_partition or "aws"
116
+ region = cli_region or saved.default_region or "us-east-1"
117
+
118
+ context = AppContext(
119
+ profile_name=profile,
120
+ region_name=region,
121
+ partition=partition,
122
+ mode=ExecutionMode(getattr(args, "mode", saved.default_mode or ExecutionMode.BOTO3.value)),
123
+ endpoint_config=EndpointConfig.for_mode(
124
+ EndpointMode(getattr(args, "endpoint_mode", saved.default_endpoint_mode or EndpointMode.AWS.value)),
125
+ getattr(args, "endpoint_url", None),
126
+ ),
127
+ )
128
+
129
+ profiles = available_profiles()
130
+ regions = available_regions(partition=partition)
131
+ window = create_main_window(context, profiles=profiles, regions=regions)
132
+ window.run()
133
+ return 0
134
+
135
+
136
+ def run_doctor(args: argparse.Namespace) -> int:
137
+ """Print environment diagnostics."""
138
+ print(f"gui4aws version: {__version__}")
139
+ print(f"python: {sys.version.split()[0]}")
140
+ if args.check_boto3 or not any([args.check_aws_cli, args.check_docker]):
141
+ print(f"boto3: {boto3.__version__}")
142
+ print(f"botocore: {botocore.__version__}")
143
+ if args.check_aws_cli or not any([args.check_boto3, args.check_docker]):
144
+ aws_path = shutil.which("aws")
145
+ print(f"aws CLI: {aws_path or 'not found on PATH'}")
146
+ if args.check_docker:
147
+ docker_path = shutil.which("docker")
148
+ print(f"docker: {docker_path or 'not found on PATH'}")
149
+ try:
150
+ import moto # pylint: disable=unused-import # noqa: F401
151
+
152
+ print("moto: importable")
153
+ except ImportError:
154
+ print("moto: not importable (install dev deps to use moto-backed tests)")
155
+ profiles = available_profiles()
156
+ print(f"profiles: {', '.join(profiles) if profiles else '(none configured)'}")
157
+ return 0
158
+
159
+
160
+ def run_list_profiles() -> int:
161
+ """Print available AWS profiles, one per line."""
162
+ for profile in available_profiles():
163
+ print(profile)
164
+ return 0
165
+
166
+
167
+ def run_list_regions(args: argparse.Namespace | None = None) -> int:
168
+ """Print known AWS regions, one per line."""
169
+ partition = getattr(args, "partition", "aws") if args else "aws"
170
+ for region in available_regions(partition=partition):
171
+ print(region)
172
+ return 0
173
+
174
+
175
+ def available_profiles() -> list[str]:
176
+ """Best-effort profile list from boto3's session."""
177
+ try:
178
+ session = boto3.Session()
179
+ return list(session.available_profiles)
180
+ except Exception: # pylint: disable=broad-exception-caught
181
+ return []
182
+
183
+
184
+ def available_regions(service: str = "ec2", partition: str = "aws") -> list[str]:
185
+ """Known regions for ``service`` in ``partition`` (EC2 is the most-supported default)."""
186
+ try:
187
+ session = boto3.Session()
188
+ return sorted(session.get_available_regions(service, partition_name=partition))
189
+ except Exception: # pylint: disable=broad-exception-caught
190
+ return []
191
+
192
+
193
+ if __name__ == "__main__":
194
+ raise SystemExit(main())
gui4aws/config.py ADDED
@@ -0,0 +1,139 @@
1
+ """User configuration: load/save preferences in a per-OS config dir.
2
+
3
+ We hand-roll a minimal TOML writer to stay stdlib-only on the write path. Reading uses
4
+ ``tomllib`` (Python 3.11+). For Python 3.10 we fall back to JSON.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ __all__ = ["AppConfig", "config_path", "load_config", "save_config"]
17
+
18
+
19
+ @dataclass
20
+ class AppConfig:
21
+ """User-visible app config."""
22
+
23
+ default_profile: str = ""
24
+ default_region: str = "us-east-1"
25
+ default_partition: str = "aws"
26
+ default_mode: str = "boto3"
27
+ default_endpoint_mode: str = "aws"
28
+ window_width: int = 1400
29
+ window_height: int = 900
30
+ history_enabled: bool = True
31
+ history_max_entries: int = 500
32
+ local_endpoints: dict[str, str] = field(default_factory=dict)
33
+
34
+
35
+ def config_path() -> Path:
36
+ """Return the OS-appropriate config file path."""
37
+ if sys.platform.startswith("win"):
38
+ base = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
39
+ return base / "gui4aws" / "config.toml"
40
+ if sys.platform == "darwin":
41
+ return Path.home() / "Library" / "Application Support" / "gui4aws" / "config.toml"
42
+ xdg = os.environ.get("XDG_CONFIG_HOME")
43
+ base = Path(xdg) if xdg else Path.home() / ".config"
44
+ return base / "gui4aws" / "config.toml"
45
+
46
+
47
+ def load_config(path: Path | None = None) -> AppConfig:
48
+ """Load config from disk, returning defaults if no file exists."""
49
+ if path is None:
50
+ path = config_path()
51
+ if not path.exists():
52
+ return AppConfig()
53
+ raw = path.read_text(encoding="utf-8")
54
+ data: dict[str, Any]
55
+ if path.suffix == ".json":
56
+ data = json.loads(raw)
57
+ else:
58
+ try:
59
+ if sys.version_info >= (3, 11):
60
+ import tomllib
61
+ else:
62
+ import tomli as tomllib
63
+
64
+ data = tomllib.loads(raw)
65
+ except ImportError: # pragma: no cover
66
+ # Fallback if neither is available (should not happen with our deps)
67
+ data = json.loads(raw)
68
+ return apply(data)
69
+
70
+
71
+ def save_config(config: AppConfig, path: Path | None = None) -> None:
72
+ """Save config to disk in TOML."""
73
+ if path is None:
74
+ path = config_path()
75
+ path.parent.mkdir(parents=True, exist_ok=True)
76
+ path.write_text(render_toml(config), encoding="utf-8")
77
+
78
+
79
+ def apply(data: dict[str, Any]) -> AppConfig:
80
+ """Map a raw dictionary (from TOML/JSON) onto an AppConfig instance."""
81
+ config = AppConfig()
82
+ for key in (
83
+ "default_profile",
84
+ "default_region",
85
+ "default_partition",
86
+ "default_mode",
87
+ "default_endpoint_mode",
88
+ ):
89
+ if key in data:
90
+ setattr(config, key, str(data[key]))
91
+ window = data.get("window", {})
92
+ if isinstance(window, dict):
93
+ if "width" in window:
94
+ config.window_width = int(window["width"])
95
+ if "height" in window:
96
+ config.window_height = int(window["height"])
97
+ history = data.get("history", {})
98
+ if isinstance(history, dict):
99
+ if "enabled" in history:
100
+ config.history_enabled = bool(history["enabled"])
101
+ if "max_entries" in history:
102
+ config.history_max_entries = int(history["max_entries"])
103
+ endpoints = data.get("local_endpoints", {})
104
+ if isinstance(endpoints, dict):
105
+ for service, value in endpoints.items():
106
+ if isinstance(value, dict) and "endpoint_url" in value:
107
+ config.local_endpoints[str(service)] = str(value["endpoint_url"])
108
+ elif isinstance(value, str):
109
+ config.local_endpoints[str(service)] = value
110
+ return config
111
+
112
+
113
+ def render_toml(config: AppConfig) -> str:
114
+ """Serialize AppConfig to a TOML string."""
115
+ lines = [
116
+ f'default_profile = "{config.default_profile}"',
117
+ f'default_region = "{config.default_region}"',
118
+ f'default_partition = "{config.default_partition}"',
119
+ f'default_mode = "{config.default_mode}"',
120
+ f'default_endpoint_mode = "{config.default_endpoint_mode}"',
121
+ "",
122
+ "[window]",
123
+ f"width = {config.window_width}",
124
+ f"height = {config.window_height}",
125
+ "",
126
+ "[history]",
127
+ f"enabled = {'true' if config.history_enabled else 'false'}",
128
+ f"max_entries = {config.history_max_entries}",
129
+ ]
130
+ for service, url in sorted(config.local_endpoints.items()):
131
+ lines.extend(
132
+ [
133
+ "",
134
+ f"[local_endpoints.{service}]",
135
+ f'endpoint_url = "{url}"',
136
+ ]
137
+ )
138
+ lines.append("")
139
+ return "\n".join(lines)
@@ -0,0 +1,79 @@
1
+ """Seed demo AWS resources so the GUI has something to browse immediately.
2
+
3
+ All resources are tagged with ``gui4aws:demo = true`` and given descriptive names so
4
+ users can immediately distinguish demo data from real infrastructure.
5
+
6
+ This package only writes resources; it never deletes. Call :func:`seed_demo_resources`
7
+ with a boto3 session (or endpoint_url for moto server mode) to create the assets.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ from gui4aws.demo_resources.aurora import seed_aurora
16
+ from gui4aws.demo_resources.backup import seed_backup
17
+ from gui4aws.demo_resources.compute import seed_lambda
18
+ from gui4aws.demo_resources.ecs import seed_ecs
19
+ from gui4aws.demo_resources.iam_resources import seed_iam_extras
20
+ from gui4aws.demo_resources.kms import seed_kms
21
+ from gui4aws.demo_resources.messaging import seed_ses, seed_sns
22
+ from gui4aws.demo_resources.monitoring import seed_cloudformation, seed_cloudwatch
23
+ from gui4aws.demo_resources.networking import seed_networking
24
+ from gui4aws.demo_resources.secrets import seed_secrets, seed_ssm
25
+ from gui4aws.demo_resources.storage import seed_s3, seed_sqs
26
+
27
+ __all__ = ["seed_demo_resources"]
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def seed_demo_resources(
33
+ *,
34
+ region_name: str = "us-east-1",
35
+ endpoint_url: str | None = None,
36
+ profile_name: str | None = None,
37
+ is_robotocore: bool = False,
38
+ ) -> dict[str, list[str]]:
39
+ """Create demo resources and return a report of what was created.
40
+
41
+ ``is_robotocore`` controls whether richer demo data (backup jobs, restore jobs,
42
+ ECS services with tasks, etc.) is seeded. Robotocore has broader API coverage
43
+ than Moto for these resource types.
44
+
45
+ Returns a dict mapping resource type to list of identifiers.
46
+ """
47
+ import boto3
48
+
49
+ session: Any
50
+ if profile_name:
51
+ session = boto3.Session(profile_name=profile_name, region_name=region_name)
52
+ else:
53
+ session = boto3.Session(region_name=region_name)
54
+
55
+ def client(service: str) -> Any:
56
+ if endpoint_url:
57
+ return session.client(service, endpoint_url=endpoint_url)
58
+ return session.client(service)
59
+
60
+ created: dict[str, list[str]] = {}
61
+
62
+ rds_client = client("rds")
63
+ created.update(seed_aurora(rds_client))
64
+ created.update(seed_backup(client("backup"), rds_client, extended=is_robotocore))
65
+ created.update(seed_networking(client("ec2"), client("elbv2")))
66
+ created.update(seed_ecs(client("ecs"), extended=is_robotocore))
67
+ created.update(seed_secrets(client("secretsmanager")))
68
+ created.update(seed_ssm(client("ssm")))
69
+ created.update(seed_kms(client("kms")))
70
+ created.update(seed_s3(client("s3")))
71
+ created.update(seed_sqs(client("sqs")))
72
+ created.update(seed_lambda(client("lambda"), client("iam")))
73
+ created.update(seed_cloudwatch(client("cloudwatch"), client("logs")))
74
+ created.update(seed_cloudformation(client("cloudformation")))
75
+ created.update(seed_sns(client("sns")))
76
+ created.update(seed_ses(client("ses")))
77
+ created.update(seed_iam_extras(client("iam")))
78
+
79
+ return created
@@ -0,0 +1,11 @@
1
+ """Shared constants and helpers used across demo resource seeders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ DEMO_TAG = {"Key": "gui4aws:demo", "Value": "true"}
6
+ DEMO_DESC_TAG_KEY = "Description"
7
+
8
+
9
+ def tags(*extra: dict[str, str]) -> list[dict[str, str]]:
10
+ """Return a tag list with the standard demo tag plus any extras."""
11
+ return [DEMO_TAG, *extra]
@@ -0,0 +1,89 @@
1
+ """Seed demo Aurora clusters, instances, and snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from gui4aws.demo_resources._common import DEMO_DESC_TAG_KEY, tags
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def seed_aurora(rds: Any) -> dict[str, list[str]]:
14
+ """Seed demo Aurora clusters, instances, and snapshots.
15
+
16
+ Creates both MySQL and PostgreSQL clusters with associated instances and a sample
17
+ snapshot to demonstrate RDS resource browsing and management in the GUI.
18
+ """
19
+ created: dict[str, list[str]] = {"aurora_clusters": [], "aurora_instances": [], "aurora_snapshots": []}
20
+
21
+ clusters = [
22
+ {
23
+ "DBClusterIdentifier": "demo-aurora-mysql-prod",
24
+ "Engine": "aurora-mysql",
25
+ "MasterUsername": "admin",
26
+ "MasterUserPassword": "DemoPass123!",
27
+ "Tags": tags(
28
+ {"Key": "Name", "Value": "demo-aurora-mysql-prod"},
29
+ {"Key": DEMO_DESC_TAG_KEY, "Value": "Demo Aurora MySQL cluster (production-like)"},
30
+ ),
31
+ },
32
+ {
33
+ "DBClusterIdentifier": "demo-aurora-pg-analytics",
34
+ "Engine": "aurora-postgresql",
35
+ "MasterUsername": "postgres",
36
+ "MasterUserPassword": "DemoPass123!",
37
+ "Tags": tags(
38
+ {"Key": "Name", "Value": "demo-aurora-pg-analytics"},
39
+ {"Key": DEMO_DESC_TAG_KEY, "Value": "Demo Aurora PostgreSQL cluster (analytics)"},
40
+ ),
41
+ },
42
+ ]
43
+
44
+ for spec in clusters:
45
+ cluster_id = str(spec["DBClusterIdentifier"])
46
+ engine = str(spec["Engine"])
47
+ try:
48
+ rds.create_db_cluster(**spec)
49
+ logger.info("created Aurora cluster %s", cluster_id)
50
+ created["aurora_clusters"].append(cluster_id)
51
+ except Exception as exc: # pylint: disable=broad-exception-caught
52
+ logger.warning("skipped cluster %s: %s", cluster_id, exc)
53
+ continue
54
+
55
+ instance_id = f"{cluster_id}-instance-1"
56
+ try:
57
+ rds.create_db_instance(
58
+ DBInstanceIdentifier=instance_id,
59
+ DBInstanceClass="db.t3.medium",
60
+ Engine=engine,
61
+ DBClusterIdentifier=cluster_id,
62
+ Tags=tags(
63
+ {"Key": "Name", "Value": instance_id},
64
+ {"Key": DEMO_DESC_TAG_KEY, "Value": f"Demo instance for {cluster_id}"},
65
+ ),
66
+ )
67
+ logger.info("created Aurora instance %s", instance_id)
68
+ created["aurora_instances"].append(instance_id)
69
+ except Exception as exc: # pylint: disable=broad-exception-caught
70
+ logger.warning("skipped instance %s: %s", instance_id, exc)
71
+
72
+ if created["aurora_clusters"]:
73
+ source = created["aurora_clusters"][0]
74
+ snap_id = f"{source}-demo-snapshot"
75
+ try:
76
+ rds.create_db_cluster_snapshot(
77
+ DBClusterIdentifier=source,
78
+ DBClusterSnapshotIdentifier=snap_id,
79
+ Tags=tags(
80
+ {"Key": "Name", "Value": snap_id},
81
+ {"Key": DEMO_DESC_TAG_KEY, "Value": "Demo snapshot — safe to restore"},
82
+ ),
83
+ )
84
+ logger.info("created Aurora snapshot %s", snap_id)
85
+ created["aurora_snapshots"].append(snap_id)
86
+ except Exception as exc: # pylint: disable=broad-exception-caught
87
+ logger.warning("skipped snapshot %s: %s", snap_id, exc)
88
+
89
+ return created