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.
- {stackport-0.1.2/stackport.egg-info → stackport-0.1.4}/PKG-INFO +32 -10
- {stackport-0.1.2 → stackport-0.1.4}/README.md +27 -9
- {stackport-0.1.2 → stackport-0.1.4}/backend/main.py +7 -2
- stackport-0.1.4/backend/routes/dynamodb.py +218 -0
- stackport-0.1.4/backend/routes/ec2.py +326 -0
- stackport-0.1.4/backend/routes/iam.py +298 -0
- stackport-0.1.4/backend/routes/lambda_svc.py +185 -0
- stackport-0.1.4/backend/routes/logs.py +216 -0
- stackport-0.1.4/backend/routes/sqs.py +306 -0
- {stackport-0.1.2 → stackport-0.1.4}/pyproject.toml +8 -1
- {stackport-0.1.2 → stackport-0.1.4/stackport.egg-info}/PKG-INFO +32 -10
- stackport-0.1.4/stackport.egg-info/SOURCES.txt +46 -0
- stackport-0.1.4/stackport.egg-info/requires.txt +8 -0
- stackport-0.1.4/tests/test_cache.py +63 -0
- stackport-0.1.4/tests/test_client.py +17 -0
- stackport-0.1.4/tests/test_config.py +41 -0
- stackport-0.1.4/tests/test_dynamodb_routes.py +255 -0
- stackport-0.1.4/tests/test_ec2_routes.py +298 -0
- stackport-0.1.4/tests/test_iam_routes.py +382 -0
- stackport-0.1.4/tests/test_lambda_routes.py +271 -0
- stackport-0.1.4/tests/test_logs_routes.py +274 -0
- stackport-0.1.4/tests/test_registries.py +69 -0
- stackport-0.1.4/tests/test_routes.py +117 -0
- stackport-0.1.4/tests/test_sqs_routes.py +290 -0
- stackport-0.1.4/ui/dist/assets/index-Co0XMz89.css +1 -0
- stackport-0.1.4/ui/dist/assets/index-R1rrNm8L.js +455 -0
- stackport-0.1.4/ui/dist/favicon.svg +68 -0
- {stackport-0.1.2 → stackport-0.1.4}/ui/dist/index.html +3 -3
- stackport-0.1.2/stackport.egg-info/SOURCES.txt +0 -27
- stackport-0.1.2/stackport.egg-info/requires.txt +0 -3
- stackport-0.1.2/ui/dist/assets/index-CRqBg0t6.js +0 -360
- stackport-0.1.2/ui/dist/assets/index-Is3dIbZM.css +0 -1
- {stackport-0.1.2 → stackport-0.1.4}/LICENSE +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/MANIFEST.in +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/__init__.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/aws_client.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/cache.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/config.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/routes/__init__.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/routes/resources.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/routes/s3.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/backend/routes/stats.py +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/setup.cfg +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.1.2 → stackport-0.1.4}/stackport.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
[](https://hub.docker.com/r/davireis/stackport)
|
|
34
|
-
[](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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
50
|
+
## Screenshots
|
|
51
|
+
|
|
52
|
+
**Dashboard** — Service overview with resource counts and health status
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
**DynamoDB Browser** — Generic resource table with search, pagination, and detail view
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
**S3 Browser** — File browser with folder navigation and object preview
|
|
59
|
+

|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
[](https://hub.docker.com/r/davireis/stackport)
|
|
6
|
-
[](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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
18
|
+
## Screenshots
|
|
19
|
+
|
|
20
|
+
**Dashboard** — Service overview with resource counts and health status
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
**DynamoDB Browser** — Generic resource table with search, pagination, and detail view
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
**S3 Browser** — File browser with folder navigation and object preview
|
|
27
|
+

|
|
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}
|