stackport 0.3.3__tar.gz → 0.3.6__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 (111) hide show
  1. {stackport-0.3.3/stackport.egg-info → stackport-0.3.6}/PKG-INFO +2 -1
  2. {stackport-0.3.3 → stackport-0.3.6}/backend/main.py +3 -2
  3. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/ec2.py +43 -0
  4. stackport-0.3.6/backend/routes/rds.py +447 -0
  5. {stackport-0.3.3 → stackport-0.3.6}/pyproject.toml +2 -2
  6. {stackport-0.3.3 → stackport-0.3.6/stackport.egg-info}/PKG-INFO +2 -1
  7. {stackport-0.3.3 → stackport-0.3.6}/stackport.egg-info/SOURCES.txt +8 -6
  8. {stackport-0.3.3 → stackport-0.3.6}/stackport.egg-info/requires.txt +1 -0
  9. {stackport-0.3.3 → stackport-0.3.6}/tests/test_ec2_routes.py +66 -0
  10. stackport-0.3.6/tests/test_rds_routes.py +554 -0
  11. stackport-0.3.6/ui/dist/assets/StateMachineGraph-DnspmX4c.js +6 -0
  12. stackport-0.3.6/ui/dist/assets/index-BoJK2una.css +1 -0
  13. stackport-0.3.6/ui/dist/assets/index-CVBAWnD3.js +652 -0
  14. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/index.html +2 -2
  15. stackport-0.3.3/ui/dist/assets/StateMachineGraph-DKisycYa.js +0 -6
  16. stackport-0.3.3/ui/dist/assets/index-CCOvZYbr.css +0 -1
  17. stackport-0.3.3/ui/dist/assets/index-XBYym2zo.js +0 -652
  18. {stackport-0.3.3 → stackport-0.3.6}/LICENSE +0 -0
  19. {stackport-0.3.3 → stackport-0.3.6}/MANIFEST.in +0 -0
  20. {stackport-0.3.3 → stackport-0.3.6}/README.md +0 -0
  21. {stackport-0.3.3 → stackport-0.3.6}/backend/__init__.py +0 -0
  22. {stackport-0.3.3 → stackport-0.3.6}/backend/aws_client.py +0 -0
  23. {stackport-0.3.3 → stackport-0.3.6}/backend/cache.py +0 -0
  24. {stackport-0.3.3 → stackport-0.3.6}/backend/cli.py +0 -0
  25. {stackport-0.3.3 → stackport-0.3.6}/backend/config.py +0 -0
  26. {stackport-0.3.3 → stackport-0.3.6}/backend/endpoint_store.py +0 -0
  27. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/__init__.py +0 -0
  28. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/common.py +0 -0
  29. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/dynamodb.py +0 -0
  30. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/endpoints.py +0 -0
  31. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/iam.py +0 -0
  32. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/lambda_svc.py +0 -0
  33. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/logs.py +0 -0
  34. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/resources.py +0 -0
  35. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/s3.py +0 -0
  36. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/secretsmanager.py +0 -0
  37. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/sqs.py +0 -0
  38. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/stats.py +0 -0
  39. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/stepfunctions.py +0 -0
  40. {stackport-0.3.3 → stackport-0.3.6}/backend/routes/tags.py +0 -0
  41. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/__init__.py +0 -0
  42. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/dynamodb.py +0 -0
  43. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/endpoints.py +0 -0
  44. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/lambda_svc.py +0 -0
  45. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/s3.py +0 -0
  46. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/secretsmanager.py +0 -0
  47. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/sqs.py +0 -0
  48. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/stepfunctions.py +0 -0
  49. {stackport-0.3.3 → stackport-0.3.6}/backend/schemas/tags.py +0 -0
  50. {stackport-0.3.3 → stackport-0.3.6}/backend/websocket.py +0 -0
  51. {stackport-0.3.3 → stackport-0.3.6}/setup.cfg +0 -0
  52. {stackport-0.3.3 → stackport-0.3.6}/stackport.egg-info/dependency_links.txt +0 -0
  53. {stackport-0.3.3 → stackport-0.3.6}/stackport.egg-info/entry_points.txt +0 -0
  54. {stackport-0.3.3 → stackport-0.3.6}/stackport.egg-info/top_level.txt +0 -0
  55. {stackport-0.3.3 → stackport-0.3.6}/tests/test_cache.py +0 -0
  56. {stackport-0.3.3 → stackport-0.3.6}/tests/test_cli.py +0 -0
  57. {stackport-0.3.3 → stackport-0.3.6}/tests/test_client.py +0 -0
  58. {stackport-0.3.3 → stackport-0.3.6}/tests/test_config.py +0 -0
  59. {stackport-0.3.3 → stackport-0.3.6}/tests/test_dynamodb_routes.py +0 -0
  60. {stackport-0.3.3 → stackport-0.3.6}/tests/test_endpoint_store.py +0 -0
  61. {stackport-0.3.3 → stackport-0.3.6}/tests/test_endpoints.py +0 -0
  62. {stackport-0.3.3 → stackport-0.3.6}/tests/test_iam_routes.py +0 -0
  63. {stackport-0.3.3 → stackport-0.3.6}/tests/test_lambda_routes.py +0 -0
  64. {stackport-0.3.3 → stackport-0.3.6}/tests/test_logs_routes.py +0 -0
  65. {stackport-0.3.3 → stackport-0.3.6}/tests/test_multi_endpoint.py +0 -0
  66. {stackport-0.3.3 → stackport-0.3.6}/tests/test_readonly_middleware.py +0 -0
  67. {stackport-0.3.3 → stackport-0.3.6}/tests/test_registries.py +0 -0
  68. {stackport-0.3.3 → stackport-0.3.6}/tests/test_routes.py +0 -0
  69. {stackport-0.3.3 → stackport-0.3.6}/tests/test_s3_routes.py +0 -0
  70. {stackport-0.3.3 → stackport-0.3.6}/tests/test_s3_upload_limit_env.py +0 -0
  71. {stackport-0.3.3 → stackport-0.3.6}/tests/test_secretsmanager_routes.py +0 -0
  72. {stackport-0.3.3 → stackport-0.3.6}/tests/test_sqs_routes.py +0 -0
  73. {stackport-0.3.3 → stackport-0.3.6}/tests/test_tags_routes.py +0 -0
  74. {stackport-0.3.3 → stackport-0.3.6}/tests/test_websocket.py +0 -0
  75. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  76. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/acm.svg +0 -0
  77. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/apigateway.svg +0 -0
  78. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/appsync.svg +0 -0
  79. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/athena.svg +0 -0
  80. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/cloudformation.svg +0 -0
  81. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/cloudfront.svg +0 -0
  82. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  83. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/dynamodb.svg +0 -0
  84. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/ec2.svg +0 -0
  85. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/ecr.svg +0 -0
  86. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/ecs.svg +0 -0
  87. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/elasticache.svg +0 -0
  88. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  89. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  90. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  91. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/events.svg +0 -0
  92. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/firehose.svg +0 -0
  93. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/glue.svg +0 -0
  94. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/iam.svg +0 -0
  95. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/kinesis.svg +0 -0
  96. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/kms.svg +0 -0
  97. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/lambda.svg +0 -0
  98. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/logs.svg +0 -0
  99. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/monitoring.svg +0 -0
  100. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/rds.svg +0 -0
  101. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/route53.svg +0 -0
  102. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/s3.svg +0 -0
  103. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  104. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/ses.svg +0 -0
  105. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/sns.svg +0 -0
  106. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/sqs.svg +0 -0
  107. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/ssm.svg +0 -0
  108. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  109. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/aws-icons/wafv2.svg +0 -0
  110. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/favicon.png +0 -0
  111. {stackport-0.3.3 → stackport-0.3.6}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.3
3
+ Version: 0.3.6
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -29,6 +29,7 @@ Requires-Dist: websockets>=12.0
29
29
  Requires-Dist: python-multipart>=0.0.26
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
32
33
  Requires-Dist: httpx>=0.27.0; extra == "dev"
33
34
  Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
34
35
  Dynamic: license-file
@@ -17,7 +17,7 @@ from botocore.exceptions import (
17
17
  )
18
18
 
19
19
  from backend.config import LOG_LEVEL, STACKPORT_ALLOW_WRITES, STACKPORT_PORT
20
- from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, logs, resources, s3, secretsmanager, sqs, stats, stepfunctions, tags
20
+ from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, logs, resources, rds, s3, secretsmanager, sqs, stats, stepfunctions, tags
21
21
  from backend.websocket import probe_loop, websocket_endpoint
22
22
 
23
23
 
@@ -120,12 +120,13 @@ app.include_router(dynamodb.router, prefix="/api/dynamodb")
120
120
  app.include_router(lambda_svc.router, prefix="/api/lambda", tags=["lambda"])
121
121
  app.include_router(sqs.router, prefix="/api/sqs", tags=["sqs"])
122
122
  app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
123
- app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
123
+ app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2", "autoscaling"])
124
124
  app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
125
125
  app.include_router(secretsmanager.router, prefix="/api/secretsmanager", tags=["secretsmanager"])
126
126
  app.include_router(stepfunctions.router, prefix="/api/stepfunctions", tags=["stepfunctions"])
127
127
  app.include_router(tags.router, prefix="/api", tags=["tags"])
128
128
  app.include_router(resources.router, prefix="/api")
129
+ app.include_router(rds.router, prefix="/api/rds", tags=["rds"])
129
130
 
130
131
 
131
132
  # WebSocket endpoint for real-time updates
@@ -38,6 +38,49 @@ def _decode_user_data(encoded: str | None) -> str | None:
38
38
  except Exception:
39
39
  return None
40
40
 
41
+ def _map_asg_instance(inst: dict) -> dict:
42
+ """Map an Auto Scaling Group instance to a camelCase shape."""
43
+ return {
44
+ "instanceId": inst.get("InstanceId"),
45
+ "instanceType": inst.get("InstanceType"),
46
+ "lifecycleState": inst.get("LifecycleState"),
47
+ "healthStatus": inst.get("HealthStatus"),
48
+ "availabilityZone": inst.get("AvailabilityZone"),
49
+ }
50
+
51
+
52
+ @router.get("/asgs")
53
+ def list_autoscaling_groups(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
54
+ """List all Auto Scaling Groups with their instances."""
55
+ try:
56
+ client = get_client("autoscaling", **ep.client_kwargs())
57
+ paginator = client.get_paginator("describe_auto_scaling_groups")
58
+
59
+ all_groups = []
60
+ for page in paginator.paginate():
61
+ for group in page.get("AutoScalingGroups", []):
62
+ created_time = group.get("CreatedTime")
63
+ instances = group.get("Instances", [])
64
+ all_groups.append(
65
+ {
66
+ "autoScalingGroupARN": group["AutoScalingGroupARN"],
67
+ "autoScalingGroupName": group["AutoScalingGroupName"],
68
+ "createdTime": created_time.isoformat() if created_time else None,
69
+ "desiredCapacity": group["DesiredCapacity"],
70
+ "maxSize": group["MaxSize"],
71
+ "minSize": group["MinSize"],
72
+ "availabilityZones": group.get("AvailabilityZones", []),
73
+ "healthCheckGracePeriod": group.get("HealthCheckGracePeriod", 0),
74
+ "instanceCount": len(instances),
75
+ "instances": [_map_asg_instance(i) for i in instances],
76
+ "loadBalancerNames": group.get("LoadBalancerNames", []),
77
+ "tags": group.get("Tags", []),
78
+ }
79
+ )
80
+ return {"auto_scaling_groups": all_groups}
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+
41
84
 
42
85
  @router.get("/instances")
43
86
  def list_instances(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
@@ -0,0 +1,447 @@
1
+ from typing import Any
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, Query
4
+
5
+ from backend.aws_client import get_client
6
+ from backend.routes.common import EndpointInfo, get_endpoint_info
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ def _flatten_db_instances(response: dict) -> list[dict]:
12
+ """Extract DBInstances from describe_db_instances response."""
13
+ return response.get("DBInstances", [])
14
+
15
+
16
+ def _flatten_db_clusters(response: dict) -> list[dict]:
17
+ """Extract DBClusters from describe_db_clusters response."""
18
+ return response.get("DBClusters", [])
19
+
20
+
21
+ def _get_endpoint_address(instance: dict) -> str:
22
+ """Get the endpoint address from an RDS instance."""
23
+ endpoint = instance.get("Endpoint", {})
24
+ if isinstance(endpoint, dict):
25
+ return endpoint.get("Address", "")
26
+ return ""
27
+
28
+
29
+ def _get_endpoint_port(instance: dict) -> int:
30
+ """Get the endpoint port from an RDS instance."""
31
+ endpoint = instance.get("Endpoint", {})
32
+ if isinstance(endpoint, dict):
33
+ return endpoint.get("Port", 0)
34
+ return 0
35
+
36
+
37
+ def _get_reader_endpoint_address(cluster: dict) -> str:
38
+ """Get the reader endpoint address from an RDS cluster."""
39
+ endpoint = cluster.get("ReaderEndpoint", "")
40
+ return endpoint if isinstance(endpoint, str) else ""
41
+
42
+
43
+ @router.get("/instances")
44
+ def list_db_instances(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
45
+ """List all RDS DB instances with enriched metadata."""
46
+ try:
47
+ client = get_client("rds", **ep.client_kwargs())
48
+ paginator = client.get_paginator("describe_db_instances")
49
+
50
+ all_instances = []
51
+ for page in paginator.paginate():
52
+ instances = _flatten_db_instances(page)
53
+ for instance in instances:
54
+ endpoint_addr = _get_endpoint_address(instance)
55
+ endpoint_port = _get_endpoint_port(instance)
56
+
57
+ all_instances.append(
58
+ {
59
+ "dbInstanceIdentifier": instance["DBInstanceIdentifier"],
60
+ "dbInstanceClass": instance.get("DBInstanceClass", ""),
61
+ "engine": instance.get("Engine", ""),
62
+ "engineVersion": instance.get("EngineVersion", ""),
63
+ "status": instance.get("DBInstanceStatus", ""),
64
+ "masterUsername": instance.get("MasterUsername", ""),
65
+ "endpoint": endpoint_addr,
66
+ "port": endpoint_port,
67
+ "multiAz": instance.get("MultiAZ", False),
68
+ "availabilityZone": instance.get("AvailabilityZone", ""),
69
+ "storageType": instance.get("StorageType", ""),
70
+ "allocatedStorage": instance.get("AllocatedStorage", 0),
71
+ "storageEncrypted": instance.get("StorageEncrypted", False),
72
+ "publiclyAccessible": instance.get("PubliclyAccessible", False),
73
+ "vpcSecurityGroups": instance.get("VpcSecurityGroups", []),
74
+ "dbSubnetGroup": instance.get("DBSubnetGroup", {}),
75
+ "parameterGroup": instance.get("DBParameterGroups", [{}])[0] if instance.get("DBParameterGroups") else {},
76
+ "tags": instance.get("TagList", []),
77
+ "createdTime": instance.get("InstanceCreateTime"),
78
+ "readReplicaSourceIdentifier": instance.get("ReadReplicaSourceDBInstanceIdentifier"),
79
+ "readReplicaIdentifiers": instance.get("ReadReplicaDBInstanceIdentifiers", []),
80
+ }
81
+ )
82
+
83
+ return {"instances": all_instances}
84
+ except Exception as e:
85
+ raise HTTPException(status_code=500, detail=str(e))
86
+
87
+
88
+ @router.get("/instances/{instance_id}")
89
+ def get_db_instance_detail(instance_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
90
+ """Get detailed information for a specific DB instance."""
91
+ try:
92
+ client = get_client("rds", **ep.client_kwargs())
93
+
94
+ response = client.describe_db_instances(DBInstanceIdentifier=instance_id)
95
+ instances = _flatten_db_instances(response)
96
+
97
+ if not instances:
98
+ raise HTTPException(status_code=404, detail=f"DB Instance {instance_id} not found")
99
+
100
+ instance = instances[0]
101
+ endpoint_addr = _get_endpoint_address(instance)
102
+ endpoint_port = _get_endpoint_port(instance)
103
+
104
+ # Get tags separately
105
+ try:
106
+ tags_response = client.list_tags_for_resource(ResourceName=instance.get("DbiResourceId", ""))
107
+ tags = tags_response.get("TagList", [])
108
+ except Exception:
109
+ tags = instance.get("TagList", [])
110
+
111
+ return {
112
+ "instance": {
113
+ "dbInstanceIdentifier": instance["DBInstanceIdentifier"],
114
+ "dbInstanceClass": instance.get("DBInstanceClass", ""),
115
+ "engine": instance.get("Engine", ""),
116
+ "engineVersion": instance.get("EngineVersion", ""),
117
+ "status": instance.get("DBInstanceStatus", ""),
118
+ "masterUsername": instance.get("MasterUsername", ""),
119
+ "endpoint": endpoint_addr,
120
+ "port": endpoint_port,
121
+ "multiAz": instance.get("MultiAZ", False),
122
+ "availabilityZone": instance.get("AvailabilityZone", ""),
123
+ "storageType": instance.get("StorageType", ""),
124
+ "allocatedStorage": instance.get("AllocatedStorage", 0),
125
+ "iops": instance.get("Iops"),
126
+ "storageEncrypted": instance.get("StorageEncrypted", False),
127
+ "kmsKeyId": instance.get("KmsKeyId"),
128
+ "publiclyAccessible": instance.get("PubliclyAccessible", False),
129
+ "vpcSecurityGroups": instance.get("VpcSecurityGroups", []),
130
+ "dbSubnetGroup": instance.get("DBSubnetGroup", {}),
131
+ "dbParameterGroups": instance.get("DBParameterGroups", []),
132
+ "optionGroupMemberships": instance.get("OptionGroupMemberships", []),
133
+ "tags": tags,
134
+ "createdTime": instance.get("InstanceCreateTime"),
135
+ "backupRetentionPeriod": instance.get("BackupRetentionPeriod", 0),
136
+ "preferredBackupWindow": instance.get("PreferredBackupWindow", ""),
137
+ "preferredMaintenanceWindow": instance.get("PreferredMaintenanceWindow", ""),
138
+ "readReplicaSourceIdentifier": instance.get("ReadReplicaSourceDBInstanceIdentifier"),
139
+ "readReplicaIdentifiers": instance.get("ReadReplicaDBInstanceIdentifiers", []),
140
+ "certificateDetails": instance.get("CertificateDetails", {}),
141
+ "pendingModifiedValues": instance.get("PendingModifiedValues", {}),
142
+ "latestRestorableTime": instance.get("LatestRestorableTime"),
143
+ "earliestRestorableTime": instance.get("EarliestRestorableTime"),
144
+ }
145
+ }
146
+ except HTTPException:
147
+ raise
148
+ except Exception as e:
149
+ error_msg = str(e)
150
+ if "DBInstanceNotFound" in error_msg:
151
+ raise HTTPException(status_code=404, detail=f"DB Instance {instance_id} not found")
152
+ raise HTTPException(status_code=500, detail=str(e))
153
+
154
+
155
+ @router.get("/clusters")
156
+ def list_db_clusters(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
157
+ """List all RDS DB clusters with enriched metadata."""
158
+ try:
159
+ client = get_client("rds", **ep.client_kwargs())
160
+ paginator = client.get_paginator("describe_db_clusters")
161
+
162
+ all_clusters = []
163
+ for page in paginator.paginate():
164
+ clusters = _flatten_db_clusters(page)
165
+ for cluster in clusters:
166
+ all_clusters.append(
167
+ {
168
+ "dbClusterIdentifier": cluster["DBClusterIdentifier"],
169
+ "engine": cluster.get("Engine", ""),
170
+ "engineVersion": cluster.get("EngineVersion", ""),
171
+ "status": cluster.get("Status", ""),
172
+ "masterUsername": cluster.get("MasterUsername", ""),
173
+ "endpoint": cluster.get("Endpoint", ""),
174
+ "readerEndpoint": cluster.get("ReaderEndpoint", ""),
175
+ "port": cluster.get("Port", 0),
176
+ "multiAz": cluster.get("MultiAZ", False),
177
+ "storageType": cluster.get("StorageType", ""),
178
+ "allocatedStorage": cluster.get("AllocatedStorage", 0),
179
+ "storageEncrypted": cluster.get("StorageEncrypted", False),
180
+ "vpcSecurityGroups": cluster.get("VpcSecurityGroups", []),
181
+ "dbSubnetGroup": cluster.get("DBSubnetGroup", ""),
182
+ "parameterGroup": cluster.get("DBClusterParameterGroup", ""),
183
+ "tags": cluster.get("TagList", []),
184
+ "createdTime": cluster.get("ClusterCreateTime"),
185
+ "earliestRestorableTime": cluster.get("EarliestRestorableTime"),
186
+ "latestRestorableTime": cluster.get("LatestRestorableTime"),
187
+ "backupRetentionPeriod": cluster.get("BackupRetentionPeriod", 0),
188
+ "preferredBackupWindow": cluster.get("PreferredBackupWindow", ""),
189
+ "preferredMaintenanceWindow": cluster.get("PreferredMaintenanceWindow", ""),
190
+ "readReplicaIdentifiers": cluster.get("ReadReplicaDBClusterIdentifiers", []),
191
+ "dbClusterMembers": cluster.get("DBClusterMembers", []),
192
+ "serverlessV2ScalingConfiguration": cluster.get("ServerlessV2ScalingConfiguration", {}),
193
+ }
194
+ )
195
+
196
+ return {"clusters": all_clusters}
197
+ except Exception as e:
198
+ raise HTTPException(status_code=500, detail=str(e))
199
+
200
+
201
+ @router.get("/clusters/{cluster_id}")
202
+ def get_db_cluster_detail(cluster_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
203
+ """Get detailed information for a specific DB cluster."""
204
+ try:
205
+ client = get_client("rds", **ep.client_kwargs())
206
+
207
+ response = client.describe_db_clusters(DBClusterIdentifier=cluster_id)
208
+ clusters = _flatten_db_clusters(response)
209
+
210
+ if not clusters:
211
+ raise HTTPException(status_code=404, detail=f"DB Cluster {cluster_id} not found")
212
+
213
+ cluster = clusters[0]
214
+
215
+ # Get tags separately
216
+ try:
217
+ tags_response = client.list_tags_for_resource(ResourceName=cluster.get("DbClusterResourceId", ""))
218
+ tags = tags_response.get("TagList", [])
219
+ except Exception:
220
+ tags = cluster.get("TagList", [])
221
+
222
+ return {
223
+ "cluster": {
224
+ "dbClusterIdentifier": cluster["DBClusterIdentifier"],
225
+ "engine": cluster.get("Engine", ""),
226
+ "engineVersion": cluster.get("EngineVersion", ""),
227
+ "status": cluster.get("Status", ""),
228
+ "masterUsername": cluster.get("MasterUsername", ""),
229
+ "endpoint": cluster.get("Endpoint", ""),
230
+ "readerEndpoint": cluster.get("ReaderEndpoint", ""),
231
+ "port": cluster.get("Port", 0),
232
+ "multiAz": cluster.get("MultiAZ", False),
233
+ "storageType": cluster.get("StorageType", ""),
234
+ "allocatedStorage": cluster.get("AllocatedStorage", 0),
235
+ "storageEncrypted": cluster.get("StorageEncrypted", False),
236
+ "kmsKeyId": cluster.get("KmsKeyId"),
237
+ "vpcSecurityGroups": cluster.get("VpcSecurityGroups", []),
238
+ "dbSubnetGroup": cluster.get("DBSubnetGroup", ""),
239
+ "dbClusterParameterGroup": cluster.get("DBClusterParameterGroup", ""),
240
+ "optionGroupMemberships": cluster.get("OptionGroupMemberships", []),
241
+ "tags": tags,
242
+ "createdTime": cluster.get("ClusterCreateTime"),
243
+ "earliestRestorableTime": cluster.get("EarliestRestorableTime"),
244
+ "latestRestorableTime": cluster.get("LatestRestorableTime"),
245
+ "backupRetentionPeriod": cluster.get("BackupRetentionPeriod", 0),
246
+ "preferredBackupWindow": cluster.get("PreferredBackupWindow", ""),
247
+ "preferredMaintenanceWindow": cluster.get("PreferredMaintenanceWindow", ""),
248
+ "readReplicaIdentifiers": cluster.get("ReadReplicaDBClusterIdentifiers", []),
249
+ "dbClusterMembers": cluster.get("DBClusterMembers", []),
250
+ "serverlessV2ScalingConfiguration": cluster.get("ServerlessV2ScalingConfiguration", {}),
251
+ "scalingConfigurationInfo": cluster.get("ScalingConfigurationInfo", {}),
252
+ "pendingModifiedValues": cluster.get("PendingModifiedValues", {}),
253
+ }
254
+ }
255
+ except HTTPException:
256
+ raise
257
+ except Exception as e:
258
+ error_msg = str(e)
259
+ if "DBClusterNotFound" in error_msg:
260
+ raise HTTPException(status_code=404, detail=f"DB Cluster {cluster_id} not found")
261
+ raise HTTPException(status_code=500, detail=str(e))
262
+
263
+
264
+ @router.get("/snapshots")
265
+ def list_db_snapshots(
266
+ instance_id: str | None = Query(None, description="Filter by DB instance identifier"),
267
+ cluster_id: str | None = Query(None, description="Filter by DB cluster identifier"),
268
+ snapshot_type: str | None = Query(None, description="Filter by snapshot type (automated/manual)"),
269
+ ep: EndpointInfo = Depends(get_endpoint_info),
270
+ ) -> dict[str, Any]:
271
+ """List RDS DB snapshots with optional filters."""
272
+ try:
273
+ client = get_client("rds", **ep.client_kwargs())
274
+
275
+ snapshots = []
276
+
277
+ if instance_id:
278
+ # Get snapshots for a specific instance
279
+ paginator = client.get_paginator("describe_db_snapshots")
280
+ for page in paginator.paginate(DBInstanceIdentifier=instance_id):
281
+ snapshots.extend(page.get("DBSnapshots", []))
282
+ elif cluster_id:
283
+ # Get cluster snapshots
284
+ paginator = client.get_paginator("describe_db_cluster_snapshots")
285
+ for page in paginator.paginate(DBClusterIdentifier=cluster_id):
286
+ snapshots.extend(page.get("DBClusterSnapshots", []))
287
+ elif snapshot_type:
288
+ # Get snapshots by type
289
+ paginator = client.get_paginator("describe_db_snapshots")
290
+ for page in paginator.paginate(SnapshotType=snapshot_type):
291
+ snapshots.extend(page.get("DBSnapshots", []))
292
+ else:
293
+ # Get all snapshots (instance snapshots)
294
+ paginator = client.get_paginator("describe_db_snapshots")
295
+ for page in paginator.paginate():
296
+ snapshots.extend(page.get("DBSnapshots", []))
297
+
298
+ result = []
299
+ for snapshot in snapshots:
300
+ # Handle both DBSnapshot and DBClusterSnapshot
301
+ is_cluster = "DBClusterSnapshotIdentifier" in snapshot
302
+ result.append(
303
+ {
304
+ "snapshotIdentifier": snapshot.get("DBSnapshotIdentifier" if not is_cluster else "DBClusterSnapshotIdentifier", ""),
305
+ "snapshotType": snapshot.get("SnapshotType", ""),
306
+ "status": snapshot.get("Status", ""),
307
+ "sourceType": "cluster" if is_cluster else "instance",
308
+ "sourceIdentifier": snapshot.get("DBInstanceIdentifier" if not is_cluster else "DBClusterIdentifier", ""),
309
+ "engine": snapshot.get("Engine", ""),
310
+ "engineVersion": snapshot.get("EngineVersion", ""),
311
+ "allocatedStorage": snapshot.get("AllocatedStorage", 0),
312
+ "snapshotCreateTime": snapshot.get("SnapshotCreateTime"),
313
+ "snapshotSize": snapshot.get("SnapshotSize", 0),
314
+ "encrypted": snapshot.get("Encrypted", False),
315
+ "kmsKeyId": snapshot.get("KmsKeyId"),
316
+ "tags": snapshot.get("TagList", []),
317
+ }
318
+ )
319
+
320
+ return {"snapshots": result}
321
+ except Exception as e:
322
+ raise HTTPException(status_code=500, detail=str(e))
323
+
324
+
325
+ @router.get("/parameter-groups")
326
+ def list_db_parameter_groups(
327
+ source: str = Query("all", description="Source type: all, instance, or cluster"),
328
+ ep: EndpointInfo = Depends(get_endpoint_info),
329
+ ) -> dict[str, Any]:
330
+ """List RDS DB parameter groups."""
331
+ try:
332
+ client = get_client("rds", **ep.client_kwargs())
333
+
334
+ parameter_groups = []
335
+
336
+ if source in ("all", "instance"):
337
+ # Get instance parameter groups
338
+ paginator = client.get_paginator("describe_db_parameter_groups")
339
+ for page in paginator.paginate():
340
+ for group in page.get("DBParameterGroups", []):
341
+ parameter_groups.append(
342
+ {
343
+ "name": group.get("DBParameterGroupName", ""),
344
+ "family": group.get("DBParameterGroupFamily", ""),
345
+ "description": group.get("Description", ""),
346
+ "source": "instance",
347
+ "tags": group.get("TagList", []),
348
+ }
349
+ )
350
+
351
+ if source in ("all", "cluster"):
352
+ # Get cluster parameter groups
353
+ paginator = client.get_paginator("describe_db_cluster_parameter_groups")
354
+ for page in paginator.paginate():
355
+ for group in page.get("DBClusterParameterGroups", []):
356
+ parameter_groups.append(
357
+ {
358
+ "name": group.get("DBClusterParameterGroupName", ""),
359
+ "family": group.get("DBClusterParameterGroupFamily", ""),
360
+ "description": group.get("Description", ""),
361
+ "source": "cluster",
362
+ "tags": group.get("TagList", []),
363
+ }
364
+ )
365
+
366
+ return {"parameterGroups": parameter_groups}
367
+ except Exception as e:
368
+ raise HTTPException(status_code=500, detail=str(e))
369
+
370
+
371
+ @router.get("/parameter-groups/{group_name}")
372
+ def get_db_parameter_group_detail(
373
+ group_name: str,
374
+ source: str = Query("instance", description="Source type: instance or cluster"),
375
+ ep: EndpointInfo = Depends(get_endpoint_info),
376
+ ) -> dict[str, Any]:
377
+ """Get detailed information for a specific parameter group including parameters."""
378
+ try:
379
+ client = get_client("rds", **ep.client_kwargs())
380
+
381
+ if source == "cluster":
382
+ # Get cluster parameter group details
383
+ response = client.describe_db_cluster_parameters(DBClusterParameterGroupName=group_name)
384
+ parameters = response.get("Parameters", [])
385
+
386
+ # Get group info
387
+ groups_response = client.describe_db_cluster_parameter_groups(DBClusterParameterGroupName=group_name)
388
+ groups = groups_response.get("DBClusterParameterGroups", [])
389
+ group_info = groups[0] if groups else {}
390
+
391
+ return {
392
+ "parameterGroup": {
393
+ "name": group_info.get("DBClusterParameterGroupName", group_name),
394
+ "family": group_info.get("DBClusterParameterGroupFamily", ""),
395
+ "description": group_info.get("Description", ""),
396
+ "source": "cluster",
397
+ "parameters": [
398
+ {
399
+ "name": p.get("ParameterName", ""),
400
+ "value": p.get("ParameterValue", ""),
401
+ "description": p.get("Description", ""),
402
+ "dataType": p.get("DataType", ""),
403
+ "allowedValues": p.get("AllowedValues", ""),
404
+ "isModifiable": p.get("IsModifiable", False),
405
+ "applyMethod": p.get("ApplyMethod", "immediate"),
406
+ }
407
+ for p in parameters
408
+ ],
409
+ }
410
+ }
411
+ else:
412
+ # Get instance parameter group details
413
+ response = client.describe_db_parameters(DBParameterGroupName=group_name)
414
+ parameters = response.get("Parameters", [])
415
+
416
+ # Get group info
417
+ groups_response = client.describe_db_parameter_groups(DBParameterGroupName=group_name)
418
+ groups = groups_response.get("DBParameterGroups", [])
419
+ group_info = groups[0] if groups else {}
420
+
421
+ return {
422
+ "parameterGroup": {
423
+ "name": group_info.get("DBParameterGroupName", group_name),
424
+ "family": group_info.get("DBParameterGroupFamily", ""),
425
+ "description": group_info.get("Description", ""),
426
+ "source": "instance",
427
+ "parameters": [
428
+ {
429
+ "name": p.get("ParameterName", ""),
430
+ "value": p.get("ParameterValue", ""),
431
+ "description": p.get("Description", ""),
432
+ "dataType": p.get("DataType", ""),
433
+ "allowedValues": p.get("AllowedValues", ""),
434
+ "isModifiable": p.get("IsModifiable", False),
435
+ "applyMethod": p.get("ApplyMethod", "immediate"),
436
+ }
437
+ for p in parameters
438
+ ],
439
+ }
440
+ }
441
+ except HTTPException:
442
+ raise
443
+ except Exception as e:
444
+ error_msg = str(e)
445
+ if "DBParameterGroupNotFound" in error_msg or "DBClusterParameterGroupNotFound" in error_msg:
446
+ raise HTTPException(status_code=404, detail=f"Parameter Group {group_name} not found")
447
+ raise HTTPException(status_code=500, detail=str(e))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.3.3"
3
+ version = "0.3.6"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -34,7 +34,7 @@ Repository = "https://github.com/DaviReisVieira/stackport"
34
34
  Issues = "https://github.com/DaviReisVieira/stackport/issues"
35
35
 
36
36
  [project.optional-dependencies]
37
- dev = ["pytest>=8.0", "httpx>=0.27.0", "moto[s3,sqs,lambda,dynamodb,iam]>=5.0"]
37
+ dev = ["pytest>=8.0", "pytest-mock>=3.12.0", "httpx>=0.27.0", "moto[s3,sqs,lambda,dynamodb,iam]>=5.0"]
38
38
 
39
39
  [tool.pytest.ini_options]
40
40
  testpaths = ["tests"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.3
3
+ Version: 0.3.6
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -29,6 +29,7 @@ Requires-Dist: websockets>=12.0
29
29
  Requires-Dist: python-multipart>=0.0.26
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
32
33
  Requires-Dist: httpx>=0.27.0; extra == "dev"
33
34
  Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
34
35
  Dynamic: license-file
@@ -13,9 +13,9 @@ backend/websocket.py
13
13
  backend/../ui/dist/favicon.png
14
14
  backend/../ui/dist/favicon.svg
15
15
  backend/../ui/dist/index.html
16
- backend/../ui/dist/assets/StateMachineGraph-DKisycYa.js
17
- backend/../ui/dist/assets/index-CCOvZYbr.css
18
- backend/../ui/dist/assets/index-XBYym2zo.js
16
+ backend/../ui/dist/assets/StateMachineGraph-DnspmX4c.js
17
+ backend/../ui/dist/assets/index-BoJK2una.css
18
+ backend/../ui/dist/assets/index-CVBAWnD3.js
19
19
  backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
20
20
  backend/../ui/dist/aws-icons/acm.svg
21
21
  backend/../ui/dist/aws-icons/apigateway.svg
@@ -59,6 +59,7 @@ backend/routes/endpoints.py
59
59
  backend/routes/iam.py
60
60
  backend/routes/lambda_svc.py
61
61
  backend/routes/logs.py
62
+ backend/routes/rds.py
62
63
  backend/routes/resources.py
63
64
  backend/routes/s3.py
64
65
  backend/routes/secretsmanager.py
@@ -93,6 +94,7 @@ tests/test_iam_routes.py
93
94
  tests/test_lambda_routes.py
94
95
  tests/test_logs_routes.py
95
96
  tests/test_multi_endpoint.py
97
+ tests/test_rds_routes.py
96
98
  tests/test_readonly_middleware.py
97
99
  tests/test_registries.py
98
100
  tests/test_routes.py
@@ -105,9 +107,9 @@ tests/test_websocket.py
105
107
  ui/dist/favicon.png
106
108
  ui/dist/favicon.svg
107
109
  ui/dist/index.html
108
- ui/dist/assets/StateMachineGraph-DKisycYa.js
109
- ui/dist/assets/index-CCOvZYbr.css
110
- ui/dist/assets/index-XBYym2zo.js
110
+ ui/dist/assets/StateMachineGraph-DnspmX4c.js
111
+ ui/dist/assets/index-BoJK2una.css
112
+ ui/dist/assets/index-CVBAWnD3.js
111
113
  ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
112
114
  ui/dist/aws-icons/acm.svg
113
115
  ui/dist/aws-icons/apigateway.svg
@@ -7,5 +7,6 @@ python-multipart>=0.0.26
7
7
 
8
8
  [dev]
9
9
  pytest>=8.0
10
+ pytest-mock>=3.12.0
10
11
  httpx>=0.27.0
11
12
  moto[dynamodb,iam,lambda,s3,sqs]>=5.0