stackport 0.1.2__tar.gz → 0.1.4__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 (47) hide show
  1. {stackport-0.1.2/stackport.egg-info → stackport-0.1.4}/PKG-INFO +32 -10
  2. {stackport-0.1.2 → stackport-0.1.4}/README.md +27 -9
  3. {stackport-0.1.2 → stackport-0.1.4}/backend/main.py +7 -2
  4. stackport-0.1.4/backend/routes/dynamodb.py +218 -0
  5. stackport-0.1.4/backend/routes/ec2.py +326 -0
  6. stackport-0.1.4/backend/routes/iam.py +298 -0
  7. stackport-0.1.4/backend/routes/lambda_svc.py +185 -0
  8. stackport-0.1.4/backend/routes/logs.py +216 -0
  9. stackport-0.1.4/backend/routes/sqs.py +306 -0
  10. {stackport-0.1.2 → stackport-0.1.4}/pyproject.toml +8 -1
  11. {stackport-0.1.2 → stackport-0.1.4/stackport.egg-info}/PKG-INFO +32 -10
  12. stackport-0.1.4/stackport.egg-info/SOURCES.txt +46 -0
  13. stackport-0.1.4/stackport.egg-info/requires.txt +8 -0
  14. stackport-0.1.4/tests/test_cache.py +63 -0
  15. stackport-0.1.4/tests/test_client.py +17 -0
  16. stackport-0.1.4/tests/test_config.py +41 -0
  17. stackport-0.1.4/tests/test_dynamodb_routes.py +255 -0
  18. stackport-0.1.4/tests/test_ec2_routes.py +298 -0
  19. stackport-0.1.4/tests/test_iam_routes.py +382 -0
  20. stackport-0.1.4/tests/test_lambda_routes.py +271 -0
  21. stackport-0.1.4/tests/test_logs_routes.py +274 -0
  22. stackport-0.1.4/tests/test_registries.py +69 -0
  23. stackport-0.1.4/tests/test_routes.py +117 -0
  24. stackport-0.1.4/tests/test_sqs_routes.py +290 -0
  25. stackport-0.1.4/ui/dist/assets/index-Co0XMz89.css +1 -0
  26. stackport-0.1.4/ui/dist/assets/index-R1rrNm8L.js +455 -0
  27. stackport-0.1.4/ui/dist/favicon.svg +68 -0
  28. {stackport-0.1.2 → stackport-0.1.4}/ui/dist/index.html +3 -3
  29. stackport-0.1.2/stackport.egg-info/SOURCES.txt +0 -27
  30. stackport-0.1.2/stackport.egg-info/requires.txt +0 -3
  31. stackport-0.1.2/ui/dist/assets/index-CRqBg0t6.js +0 -360
  32. stackport-0.1.2/ui/dist/assets/index-Is3dIbZM.css +0 -1
  33. {stackport-0.1.2 → stackport-0.1.4}/LICENSE +0 -0
  34. {stackport-0.1.2 → stackport-0.1.4}/MANIFEST.in +0 -0
  35. {stackport-0.1.2 → stackport-0.1.4}/backend/__init__.py +0 -0
  36. {stackport-0.1.2 → stackport-0.1.4}/backend/aws_client.py +0 -0
  37. {stackport-0.1.2 → stackport-0.1.4}/backend/cache.py +0 -0
  38. {stackport-0.1.2 → stackport-0.1.4}/backend/config.py +0 -0
  39. {stackport-0.1.2 → stackport-0.1.4}/backend/routes/__init__.py +0 -0
  40. {stackport-0.1.2 → stackport-0.1.4}/backend/routes/resources.py +0 -0
  41. {stackport-0.1.2 → stackport-0.1.4}/backend/routes/s3.py +0 -0
  42. {stackport-0.1.2 → stackport-0.1.4}/backend/routes/stats.py +0 -0
  43. {stackport-0.1.2 → stackport-0.1.4}/setup.cfg +0 -0
  44. {stackport-0.1.2 → stackport-0.1.4}/stackport.egg-info/dependency_links.txt +0 -0
  45. {stackport-0.1.2 → stackport-0.1.4}/stackport.egg-info/entry_points.txt +0 -0
  46. {stackport-0.1.2 → stackport-0.1.4}/stackport.egg-info/top_level.txt +0 -0
  47. {stackport-0.1.2 → stackport-0.1.4}/ui/dist/favicon.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -24,19 +24,39 @@ License-File: LICENSE
24
24
  Requires-Dist: fastapi>=0.115.0
25
25
  Requires-Dist: uvicorn>=0.30.0
26
26
  Requires-Dist: boto3>=1.35.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
30
+ Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
27
31
  Dynamic: license-file
28
32
 
29
- # StackPort
33
+ <p align="center">
34
+ <img src="docs/images/stackport_logo.svg" alt="StackPort — Universal AWS Resource Browser" width="150"/>
35
+ </p>
30
36
 
31
- [![CI](https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml/badge.svg)](https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml)
32
- [![PyPI](https://img.shields.io/pypi/v/stackport)](https://pypi.org/project/stackport/)
33
- [![Docker](https://img.shields.io/docker/pulls/davireis/stackport)](https://hub.docker.com/r/davireis/stackport)
34
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ <h1 align="center">StackPort</h1>
38
+ <p align="center"><strong>Universal AWS resource browser for local emulators. Built to work with MiniStack, and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.</strong></p>
35
39
 
36
- Universal AWS resource browser for local emulators. Built to work with [**MiniStack**](https://github.com/Nahuel990/ministack), and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.
40
+ <p align="center">
41
+ <a href="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml"><img src="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
42
+ <a href="https://pypi.org/project/stackport/"><img src="https://img.shields.io/pypi/v/stackport" alt="PyPI Version"></a>
43
+ <a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/pulls/davireis/stackport" alt="Docker Pulls"></a>
44
+ <a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/image-size/davireis/stackport/latest" alt="Docker Image Size"></a>
45
+ <a href="https://github.com/DaviReisVieira/stackport/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DaviReisVieira/stackport" alt="License"></a>
46
+ <img src="https://img.shields.io/badge/python-3.12-slim" alt="Python">
47
+ <a href="https://github.com/DaviReisVieira/stackport/stargazers"><img src="https://img.shields.io/github/stars/DaviReisVieira/stackport" alt="GitHub stars"></a>
48
+ </p>
37
49
 
38
- <!-- TODO: Add screenshot here -->
39
- <!-- ![StackPort Dashboard](docs/screenshot.png) -->
50
+ ## Screenshots
51
+
52
+ **Dashboard** — Service overview with resource counts and health status
53
+ ![StackPort Dashboard](docs/images/dashboard.jpeg)
54
+
55
+ **DynamoDB Browser** — Generic resource table with search, pagination, and detail view
56
+ ![DynamoDB Resources](docs/images/dynamo.jpeg)
57
+
58
+ **S3 Browser** — File browser with folder navigation and object preview
59
+ ![S3 Browser](docs/images/s3.jpeg)
40
60
 
41
61
  ## Features
42
62
 
@@ -62,8 +82,10 @@ stackport
62
82
 
63
83
  ### Docker Compose (MiniStack + StackPort)
64
84
 
85
+ This example uses [MiniStack](https://github.com/Nahuel990/ministack) as the emulator, but you can swap it for LocalStack, Moto, or any AWS-compatible endpoint — just update `AWS_ENDPOINT_URL`.
86
+
65
87
  ```bash
66
- curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docker-compose.yml
88
+ curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
67
89
  docker compose up -d
68
90
  # Open http://localhost:8080
69
91
  ```
@@ -1,14 +1,30 @@
1
- # StackPort
1
+ <p align="center">
2
+ <img src="docs/images/stackport_logo.svg" alt="StackPort — Universal AWS Resource Browser" width="150"/>
3
+ </p>
2
4
 
3
- [![CI](https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml/badge.svg)](https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml)
4
- [![PyPI](https://img.shields.io/pypi/v/stackport)](https://pypi.org/project/stackport/)
5
- [![Docker](https://img.shields.io/docker/pulls/davireis/stackport)](https://hub.docker.com/r/davireis/stackport)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ <h1 align="center">StackPort</h1>
6
+ <p align="center"><strong>Universal AWS resource browser for local emulators. Built to work with MiniStack, and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.</strong></p>
7
7
 
8
- Universal AWS resource browser for local emulators. Built to work with [**MiniStack**](https://github.com/Nahuel990/ministack), and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.
8
+ <p align="center">
9
+ <a href="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml"><img src="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://pypi.org/project/stackport/"><img src="https://img.shields.io/pypi/v/stackport" alt="PyPI Version"></a>
11
+ <a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/pulls/davireis/stackport" alt="Docker Pulls"></a>
12
+ <a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/image-size/davireis/stackport/latest" alt="Docker Image Size"></a>
13
+ <a href="https://github.com/DaviReisVieira/stackport/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DaviReisVieira/stackport" alt="License"></a>
14
+ <img src="https://img.shields.io/badge/python-3.12-slim" alt="Python">
15
+ <a href="https://github.com/DaviReisVieira/stackport/stargazers"><img src="https://img.shields.io/github/stars/DaviReisVieira/stackport" alt="GitHub stars"></a>
16
+ </p>
9
17
 
10
- <!-- TODO: Add screenshot here -->
11
- <!-- ![StackPort Dashboard](docs/screenshot.png) -->
18
+ ## Screenshots
19
+
20
+ **Dashboard** — Service overview with resource counts and health status
21
+ ![StackPort Dashboard](docs/images/dashboard.jpeg)
22
+
23
+ **DynamoDB Browser** — Generic resource table with search, pagination, and detail view
24
+ ![DynamoDB Resources](docs/images/dynamo.jpeg)
25
+
26
+ **S3 Browser** — File browser with folder navigation and object preview
27
+ ![S3 Browser](docs/images/s3.jpeg)
12
28
 
13
29
  ## Features
14
30
 
@@ -34,8 +50,10 @@ stackport
34
50
 
35
51
  ### Docker Compose (MiniStack + StackPort)
36
52
 
53
+ This example uses [MiniStack](https://github.com/Nahuel990/ministack) as the emulator, but you can swap it for LocalStack, Moto, or any AWS-compatible endpoint — just update `AWS_ENDPOINT_URL`.
54
+
37
55
  ```bash
38
- curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docker-compose.yml
56
+ curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
39
57
  docker compose up -d
40
58
  # Open http://localhost:8080
41
59
  ```
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import os
3
- import time
4
3
 
5
4
  import uvicorn
6
5
  from fastapi import FastAPI
@@ -9,7 +8,7 @@ from fastapi.responses import FileResponse
9
8
  from fastapi.staticfiles import StaticFiles
10
9
 
11
10
  from backend.config import STACKPORT_PORT
12
- from backend.routes import resources, s3, stats
11
+ from backend.routes import dynamodb, ec2, iam, lambda_svc, logs, resources, s3, sqs, stats
13
12
 
14
13
  logging.basicConfig(
15
14
  level=logging.INFO,
@@ -28,6 +27,12 @@ app.add_middleware(
28
27
 
29
28
  app.include_router(stats.router, prefix="/api")
30
29
  app.include_router(s3.router, prefix="/api/s3")
30
+ app.include_router(dynamodb.router, prefix="/api/dynamodb")
31
+ app.include_router(lambda_svc.router, prefix="/api/lambda", tags=["lambda"])
32
+ app.include_router(sqs.router, prefix="/api/sqs", tags=["sqs"])
33
+ app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
34
+ app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
35
+ app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
31
36
  app.include_router(resources.router, prefix="/api")
32
37
 
33
38
  # Serve UI static files — mount assets under /assets, SPA fallback for everything else
@@ -0,0 +1,218 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from fastapi import APIRouter, Query
5
+ from pydantic import BaseModel
6
+
7
+ from backend.aws_client import get_client
8
+ from backend.cache import cache
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ def _get_table_item_count(table_name: str) -> int:
16
+ """Return item count for a table. Cached 30s."""
17
+ cache_key = f"dynamodb:item_count:{table_name}"
18
+ cached = cache.get(cache_key)
19
+ if cached is not None:
20
+ return cached
21
+
22
+ dynamodb = get_client("dynamodb")
23
+ try:
24
+ resp = dynamodb.describe_table(TableName=table_name)
25
+ item_count = resp["Table"].get("ItemCount", 0)
26
+ cache.set(cache_key, item_count, ttl=30)
27
+ return item_count
28
+ except Exception:
29
+ logger.debug("Failed to get item count for %s", table_name, exc_info=True)
30
+ return 0
31
+
32
+
33
+ @router.get("/tables")
34
+ def list_tables():
35
+ dynamodb = get_client("dynamodb")
36
+ paginator = dynamodb.get_paginator("list_tables")
37
+ table_names = []
38
+
39
+ for page in paginator.paginate():
40
+ table_names.extend(page.get("TableNames", []))
41
+
42
+ tables = []
43
+ for name in table_names:
44
+ try:
45
+ resp = dynamodb.describe_table(TableName=name)
46
+ table = resp["Table"]
47
+ item_count = table.get("ItemCount", 0)
48
+ table_size = table.get("TableSizeBytes", 0)
49
+
50
+ key_schema = table.get("KeySchema", [])
51
+ partition_key = next((k["AttributeName"] for k in key_schema if k["KeyType"] == "HASH"), None)
52
+ sort_key = next((k["AttributeName"] for k in key_schema if k["KeyType"] == "RANGE"), None)
53
+
54
+ tables.append(
55
+ {
56
+ "name": name,
57
+ "status": table.get("TableStatus", "UNKNOWN"),
58
+ "item_count": item_count,
59
+ "size_bytes": table_size,
60
+ "partition_key": partition_key,
61
+ "sort_key": sort_key,
62
+ "billing_mode": table.get("BillingModeSummary", {}).get("BillingMode", "PROVISIONED"),
63
+ "created": table.get("CreationDateTime").isoformat() if table.get("CreationDateTime") else None,
64
+ }
65
+ )
66
+ except Exception:
67
+ logger.debug("Failed to describe table %s", name, exc_info=True)
68
+ continue
69
+
70
+ return {"tables": tables}
71
+
72
+
73
+ @router.get("/tables/{name}")
74
+ def get_table_detail(name: str):
75
+ dynamodb = get_client("dynamodb")
76
+
77
+ try:
78
+ resp = dynamodb.describe_table(TableName=name)
79
+ table = resp["Table"]
80
+
81
+ key_schema = table.get("KeySchema", [])
82
+ attribute_defs = {attr["AttributeName"]: attr["AttributeType"] for attr in table.get("AttributeDefinitions", [])}
83
+
84
+ partition_key = next((k["AttributeName"] for k in key_schema if k["KeyType"] == "HASH"), None)
85
+ sort_key = next((k["AttributeName"] for k in key_schema if k["KeyType"] == "RANGE"), None)
86
+
87
+ return {
88
+ "name": name,
89
+ "status": table.get("TableStatus", "UNKNOWN"),
90
+ "item_count": table.get("ItemCount", 0),
91
+ "size_bytes": table.get("TableSizeBytes", 0),
92
+ "partition_key": partition_key,
93
+ "partition_key_type": attribute_defs.get(partition_key) if partition_key else None,
94
+ "sort_key": sort_key,
95
+ "sort_key_type": attribute_defs.get(sort_key) if sort_key else None,
96
+ "billing_mode": table.get("BillingModeSummary", {}).get("BillingMode", "PROVISIONED"),
97
+ "created": table.get("CreationDateTime").isoformat() if table.get("CreationDateTime") else None,
98
+ "attribute_definitions": attribute_defs,
99
+ "key_schema": key_schema,
100
+ "global_secondary_indexes": table.get("GlobalSecondaryIndexes", []),
101
+ "local_secondary_indexes": table.get("LocalSecondaryIndexes", []),
102
+ }
103
+ except Exception as e:
104
+ logger.error("Failed to get table detail for %s: %s", name, e, exc_info=True)
105
+ return {"error": str(e)}
106
+
107
+
108
+ @router.get("/tables/{name}/items")
109
+ def scan_table(
110
+ name: str,
111
+ limit: int = Query(default=25, ge=1, le=100, description="Max items per page"),
112
+ exclusive_start_key: str = Query(default=None, description="Base64 encoded last evaluated key for pagination"),
113
+ ):
114
+ dynamodb = get_client("dynamodb")
115
+
116
+ scan_params: dict[str, Any] = {
117
+ "TableName": name,
118
+ "Limit": limit,
119
+ }
120
+
121
+ if exclusive_start_key:
122
+ import base64
123
+ import json
124
+
125
+ try:
126
+ decoded = base64.b64decode(exclusive_start_key).decode("utf-8")
127
+ scan_params["ExclusiveStartKey"] = json.loads(decoded)
128
+ except Exception:
129
+ logger.debug("Invalid exclusive_start_key", exc_info=True)
130
+
131
+ try:
132
+ resp = dynamodb.scan(**scan_params)
133
+ items = resp.get("Items", [])
134
+
135
+ last_evaluated_key = resp.get("LastEvaluatedKey")
136
+ next_token = None
137
+ if last_evaluated_key:
138
+ import base64
139
+ import json
140
+
141
+ next_token = base64.b64encode(json.dumps(last_evaluated_key).encode("utf-8")).decode("utf-8")
142
+
143
+ return {
144
+ "table": name,
145
+ "items": items,
146
+ "count": len(items),
147
+ "scanned_count": resp.get("ScannedCount", len(items)),
148
+ "next_token": next_token,
149
+ }
150
+ except Exception as e:
151
+ logger.error("Failed to scan table %s: %s", name, e, exc_info=True)
152
+ return {"error": str(e), "items": [], "count": 0}
153
+
154
+
155
+ class QueryRequest(BaseModel):
156
+ partition_key_value: str
157
+ sort_key_value: str | None = None
158
+ sort_key_operator: str = "=" # =, <, <=, >, >=, BETWEEN, BEGINS_WITH
159
+ limit: int = 25
160
+
161
+
162
+ @router.post("/tables/{name}/query")
163
+ def query_table(name: str, request: QueryRequest):
164
+ dynamodb = get_client("dynamodb")
165
+
166
+ try:
167
+ # Get table key schema
168
+ table_resp = dynamodb.describe_table(TableName=name)
169
+ table = table_resp["Table"]
170
+ key_schema = table.get("KeySchema", [])
171
+ attribute_defs = {attr["AttributeName"]: attr["AttributeType"] for attr in table.get("AttributeDefinitions", [])}
172
+
173
+ partition_key = next((k["AttributeName"] for k in key_schema if k["KeyType"] == "HASH"), None)
174
+ sort_key = next((k["AttributeName"] for k in key_schema if k["KeyType"] == "RANGE"), None)
175
+
176
+ if not partition_key:
177
+ return {"error": "No partition key found in table schema", "items": [], "count": 0}
178
+
179
+ partition_key_type = attribute_defs.get(partition_key, "S")
180
+
181
+ # Build key condition expression
182
+ key_condition = f"#{partition_key} = :pk"
183
+ expression_attr_names = {f"#{partition_key}": partition_key}
184
+ expression_attr_values = {":pk": {partition_key_type: request.partition_key_value}}
185
+
186
+ if sort_key and request.sort_key_value:
187
+ sort_key_type = attribute_defs.get(sort_key, "S")
188
+ if request.sort_key_operator == "=":
189
+ key_condition += f" AND #{sort_key} = :sk"
190
+ expression_attr_values[":sk"] = {sort_key_type: request.sort_key_value}
191
+ elif request.sort_key_operator in ("<", "<=", ">", ">="):
192
+ key_condition += f" AND #{sort_key} {request.sort_key_operator} :sk"
193
+ expression_attr_values[":sk"] = {sort_key_type: request.sort_key_value}
194
+ elif request.sort_key_operator == "BEGINS_WITH":
195
+ key_condition += f" AND begins_with(#{sort_key}, :sk)"
196
+ expression_attr_values[":sk"] = {sort_key_type: request.sort_key_value}
197
+ expression_attr_names[f"#{sort_key}"] = sort_key
198
+
199
+ query_params = {
200
+ "TableName": name,
201
+ "KeyConditionExpression": key_condition,
202
+ "ExpressionAttributeNames": expression_attr_names,
203
+ "ExpressionAttributeValues": expression_attr_values,
204
+ "Limit": request.limit,
205
+ }
206
+
207
+ resp = dynamodb.query(**query_params)
208
+ items = resp.get("Items", [])
209
+
210
+ return {
211
+ "table": name,
212
+ "items": items,
213
+ "count": len(items),
214
+ "scanned_count": resp.get("ScannedCount", len(items)),
215
+ }
216
+ except Exception as e:
217
+ logger.error("Failed to query table %s: %s", name, e, exc_info=True)
218
+ return {"error": str(e), "items": [], "count": 0}