awslabs.cost-explorer-mcp-server 0.0.1__tar.gz → 0.0.2__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.
- awslabs_cost_explorer_mcp_server-0.0.2/Dockerfile +82 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/PKG-INFO +13 -17
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/README.md +12 -16
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/awslabs/__init__.py +1 -1
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/awslabs/cost_explorer_mcp_server/__init__.py +3 -3
- awslabs_cost_explorer_mcp_server-0.0.2/awslabs/cost_explorer_mcp_server/helpers.py +259 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/awslabs/cost_explorer_mcp_server/server.py +180 -112
- awslabs_cost_explorer_mcp_server-0.0.2/docker-healthcheck.sh +12 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/pyproject.toml +2 -2
- awslabs_cost_explorer_mcp_server-0.0.2/tests/conftest.py +93 -0
- awslabs_cost_explorer_mcp_server-0.0.2/tests/test_helpers.py +282 -0
- awslabs_cost_explorer_mcp_server-0.0.2/tests/test_server.py +427 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/uv.lock +1 -1
- awslabs_cost_explorer_mcp_server-0.0.1/Dockerfile +0 -26
- awslabs_cost_explorer_mcp_server-0.0.1/awslabs/cost_explorer_mcp_server/helpers.py +0 -221
- awslabs_cost_explorer_mcp_server-0.0.1/docker-healthcheck.sh +0 -9
- awslabs_cost_explorer_mcp_server-0.0.1/tests/conftest.py +0 -160
- awslabs_cost_explorer_mcp_server-0.0.1/tests/test_helpers.py +0 -240
- awslabs_cost_explorer_mcp_server-0.0.1/tests/test_server.py +0 -373
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/.gitignore +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/.pre-commit-config.yaml +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/.python-version +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/CHANGELOG.md +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/LICENSE +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/NOTICE +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.1 → awslabs_cost_explorer_mcp_server-0.0.2}/tests/__init__.py +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
FROM public.ecr.aws/sam/build-python3.10@sha256:e78695db10ca8cb129e59e30f7dc9789b0dbd0181dba195d68419c72bac51ac1 AS uv
|
|
13
|
+
|
|
14
|
+
# Install the project into `/app`
|
|
15
|
+
WORKDIR /app
|
|
16
|
+
|
|
17
|
+
# Enable bytecode compilation
|
|
18
|
+
ENV UV_COMPILE_BYTECODE=1
|
|
19
|
+
|
|
20
|
+
# Copy from the cache instead of linking since it's a mounted volume
|
|
21
|
+
ENV UV_LINK_MODE=copy
|
|
22
|
+
|
|
23
|
+
# Prefer the system python
|
|
24
|
+
ENV UV_PYTHON_PREFERENCE=only-system
|
|
25
|
+
|
|
26
|
+
# Run without updating the uv.lock file like running with `--frozen`
|
|
27
|
+
ENV UV_FROZEN=true
|
|
28
|
+
|
|
29
|
+
# Copy the required files first
|
|
30
|
+
COPY pyproject.toml uv.lock ./
|
|
31
|
+
|
|
32
|
+
# Install the project's dependencies using the lockfile and settings
|
|
33
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
34
|
+
pip install uv && \
|
|
35
|
+
uv sync --frozen --no-install-project --no-dev --no-editable
|
|
36
|
+
|
|
37
|
+
# Then, add the rest of the project source code and install it
|
|
38
|
+
# Installing separately from its dependencies allows optimal layer caching
|
|
39
|
+
COPY . /app
|
|
40
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
41
|
+
uv sync --frozen --no-dev --no-editable
|
|
42
|
+
|
|
43
|
+
# Make the directory just in case it doesn't exist
|
|
44
|
+
RUN mkdir -p /root/.local
|
|
45
|
+
|
|
46
|
+
FROM public.ecr.aws/sam/build-python3.10@sha256:e78695db10ca8cb129e59e30f7dc9789b0dbd0181dba195d68419c72bac51ac1
|
|
47
|
+
|
|
48
|
+
# Place executables in the environment at the front of the path and include other binaries
|
|
49
|
+
ENV PATH="/app/.venv/bin:$PATH:/usr/sbin"
|
|
50
|
+
|
|
51
|
+
# Set environment variables
|
|
52
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
53
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
54
|
+
FASTMCP_LOG_LEVEL=INFO
|
|
55
|
+
|
|
56
|
+
# Install lsof for the healthcheck
|
|
57
|
+
# Install other tools as needed for the MCP server
|
|
58
|
+
# Add non-root user and ability to change directory into /root
|
|
59
|
+
RUN yum update -y && \
|
|
60
|
+
yum install -y lsof && \
|
|
61
|
+
yum clean all -y && \
|
|
62
|
+
rm -rf /var/cache/yum && \
|
|
63
|
+
groupadd --force --system app && \
|
|
64
|
+
useradd app -g app -d /app && \
|
|
65
|
+
chmod o+x /root
|
|
66
|
+
|
|
67
|
+
# Get the project from the uv layer
|
|
68
|
+
COPY --from=uv --chown=app:app /root/.local /root/.local
|
|
69
|
+
COPY --from=uv --chown=app:app /app/.venv /app/.venv
|
|
70
|
+
|
|
71
|
+
# Get healthcheck script
|
|
72
|
+
COPY ./docker-healthcheck.sh /usr/local/bin/docker-healthcheck.sh
|
|
73
|
+
RUN chmod +x /usr/local/bin/docker-healthcheck.sh
|
|
74
|
+
|
|
75
|
+
# Run as non-root
|
|
76
|
+
USER app
|
|
77
|
+
|
|
78
|
+
# Health check
|
|
79
|
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "docker-healthcheck.sh" ]
|
|
80
|
+
|
|
81
|
+
# Use entrypoint instead of CMD
|
|
82
|
+
ENTRYPOINT ["awslabs.cost-explorer-mcp-server"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awslabs.cost-explorer-mcp-server
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: MCP server for analyzing AWS costs and usage data through the AWS Cost Explorer API
|
|
5
5
|
Project-URL: Homepage, https://awslabs.github.io/mcp/
|
|
6
6
|
Project-URL: Documentation, https://awslabs.github.io/mcp/servers/cost-explorer-mcp-server/
|
|
@@ -127,6 +127,17 @@ The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment v
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
Make sure the AWS profile has permissions to access the AWS Cost Explorer API. The MCP server creates a boto3 session using the specified profile to authenticate with AWS services. Your AWS IAM credentials remain on your local machine and are strictly used for accessing AWS services.
|
|
130
|
+
## Cost Considerations
|
|
131
|
+
|
|
132
|
+
**Important:** AWS Cost Explorer API incurs charges on a per-request basis. Each API call made by this MCP server will result in charges to your AWS account.
|
|
133
|
+
|
|
134
|
+
- **Cost Explorer API Pricing:** The AWS Cost Explorer API lets you directly access the interactive, ad-hoc query engine that powers AWS Cost Explorer. Each request will incur a cost of $0.01.
|
|
135
|
+
- Each tool invocation that queries Cost Explorer (get_dimension_values, get_tag_values, get_cost_and_usage) will generate at least one billable API request
|
|
136
|
+
- Complex queries with multiple filters or large date ranges may result in multiple API calls
|
|
137
|
+
|
|
138
|
+
For current pricing information, please refer to the [AWS Cost Explorer Pricing page](https://aws.amazon.com/aws-cost-management/aws-cost-explorer/pricing/).
|
|
139
|
+
|
|
140
|
+
|
|
130
141
|
## Security Considerations
|
|
131
142
|
|
|
132
143
|
### Required IAM Permissions
|
|
@@ -135,22 +146,7 @@ The following IAM permissions are required for this MCP server:
|
|
|
135
146
|
- ce:GetDimensionValues
|
|
136
147
|
- ce:GetTags
|
|
137
148
|
|
|
138
|
-
|
|
139
|
-
json
|
|
140
|
-
{
|
|
141
|
-
"Version": "2012-10-17",
|
|
142
|
-
"Statement": [
|
|
143
|
-
{
|
|
144
|
-
"Effect": "Allow",
|
|
145
|
-
"Action": [
|
|
146
|
-
"ce:GetCostAndUsage",
|
|
147
|
-
"ce:GetDimensionValues",
|
|
148
|
-
"ce:GetTags"
|
|
149
|
-
],
|
|
150
|
-
"Resource": "*"
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
}
|
|
149
|
+
|
|
154
150
|
|
|
155
151
|
## Available Tools
|
|
156
152
|
|
|
@@ -98,6 +98,17 @@ The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment v
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
Make sure the AWS profile has permissions to access the AWS Cost Explorer API. The MCP server creates a boto3 session using the specified profile to authenticate with AWS services. Your AWS IAM credentials remain on your local machine and are strictly used for accessing AWS services.
|
|
101
|
+
## Cost Considerations
|
|
102
|
+
|
|
103
|
+
**Important:** AWS Cost Explorer API incurs charges on a per-request basis. Each API call made by this MCP server will result in charges to your AWS account.
|
|
104
|
+
|
|
105
|
+
- **Cost Explorer API Pricing:** The AWS Cost Explorer API lets you directly access the interactive, ad-hoc query engine that powers AWS Cost Explorer. Each request will incur a cost of $0.01.
|
|
106
|
+
- Each tool invocation that queries Cost Explorer (get_dimension_values, get_tag_values, get_cost_and_usage) will generate at least one billable API request
|
|
107
|
+
- Complex queries with multiple filters or large date ranges may result in multiple API calls
|
|
108
|
+
|
|
109
|
+
For current pricing information, please refer to the [AWS Cost Explorer Pricing page](https://aws.amazon.com/aws-cost-management/aws-cost-explorer/pricing/).
|
|
110
|
+
|
|
111
|
+
|
|
101
112
|
## Security Considerations
|
|
102
113
|
|
|
103
114
|
### Required IAM Permissions
|
|
@@ -106,22 +117,7 @@ The following IAM permissions are required for this MCP server:
|
|
|
106
117
|
- ce:GetDimensionValues
|
|
107
118
|
- ce:GetTags
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
json
|
|
111
|
-
{
|
|
112
|
-
"Version": "2012-10-17",
|
|
113
|
-
"Statement": [
|
|
114
|
-
{
|
|
115
|
-
"Effect": "Allow",
|
|
116
|
-
"Action": [
|
|
117
|
-
"ce:GetCostAndUsage",
|
|
118
|
-
"ce:GetDimensionValues",
|
|
119
|
-
"ce:GetTags"
|
|
120
|
-
],
|
|
121
|
-
"Resource": "*"
|
|
122
|
-
}
|
|
123
|
-
]
|
|
124
|
-
}
|
|
120
|
+
|
|
125
121
|
|
|
126
122
|
## Available Tools
|
|
127
123
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Helper functions for the Cost Explorer MCP server."""
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Set up logging
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Initialize AWS Cost Explorer client
|
|
14
|
+
ce = boto3.client("ce")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_date_format(date_str: str) -> Tuple[bool, str]:
|
|
18
|
+
"""Validate that a date string is in YYYY-MM-DD format and is a valid date.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
date_str: The date string to validate
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple of (is_valid, error_message)
|
|
25
|
+
"""
|
|
26
|
+
# Check format with regex
|
|
27
|
+
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
|
|
28
|
+
return False, f"Date '{date_str}' is not in YYYY-MM-DD format"
|
|
29
|
+
|
|
30
|
+
# Check if it's a valid date
|
|
31
|
+
try:
|
|
32
|
+
datetime.strptime(date_str, "%Y-%m-%d")
|
|
33
|
+
return True, ""
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
return False, f"Invalid date '{date_str}': {str(e)}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_dimension_values(
|
|
39
|
+
key: str, billing_period_start: str, billing_period_end: str
|
|
40
|
+
) -> Dict[str, Any]:
|
|
41
|
+
"""Get available values for a specific dimension."""
|
|
42
|
+
# Validate date formats
|
|
43
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
44
|
+
if not is_valid_start:
|
|
45
|
+
return {"error": error_start}
|
|
46
|
+
|
|
47
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
48
|
+
if not is_valid_end:
|
|
49
|
+
return {"error": error_end}
|
|
50
|
+
|
|
51
|
+
# Validate date range
|
|
52
|
+
if billing_period_start > billing_period_end:
|
|
53
|
+
return {
|
|
54
|
+
"error": f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
response = ce.get_dimension_values(
|
|
59
|
+
TimePeriod={"Start": billing_period_start, "End": billing_period_end},
|
|
60
|
+
Dimension=key.upper(),
|
|
61
|
+
)
|
|
62
|
+
dimension_values = response["DimensionValues"]
|
|
63
|
+
values = [value["Value"] for value in dimension_values]
|
|
64
|
+
return {"dimension": key.upper(), "values": values}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error getting dimension values: {e}")
|
|
67
|
+
return {"error": str(e)}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_tag_values(
|
|
71
|
+
tag_key: str, billing_period_start: str, billing_period_end: str
|
|
72
|
+
) -> Dict[str, Any]:
|
|
73
|
+
"""Get available values for a specific tag key."""
|
|
74
|
+
# Validate date formats
|
|
75
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
76
|
+
if not is_valid_start:
|
|
77
|
+
return {"error": error_start}
|
|
78
|
+
|
|
79
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
80
|
+
if not is_valid_end:
|
|
81
|
+
return {"error": error_end}
|
|
82
|
+
|
|
83
|
+
# Validate date range
|
|
84
|
+
if billing_period_start > billing_period_end:
|
|
85
|
+
return {
|
|
86
|
+
"error": f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = ce.get_tags(
|
|
91
|
+
TimePeriod={"Start": billing_period_start, "End": billing_period_end},
|
|
92
|
+
TagKey=tag_key,
|
|
93
|
+
)
|
|
94
|
+
tag_values = response["Tags"]
|
|
95
|
+
return {"tag_key": tag_key, "values": tag_values}
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error getting tag values: {e}")
|
|
98
|
+
return {"error": str(e)}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_expression(
|
|
102
|
+
expression: Dict[str, Any], billing_period_start: str, billing_period_end: str
|
|
103
|
+
) -> Dict[str, Any]:
|
|
104
|
+
"""Recursively validate the filter expression.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
expression: The filter expression to validate
|
|
108
|
+
billing_period_start: Start date of the billing period
|
|
109
|
+
billing_period_end: End date of the billing period
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Empty dictionary if valid, or an error dictionary
|
|
113
|
+
"""
|
|
114
|
+
# Validate date formats
|
|
115
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
116
|
+
if not is_valid_start:
|
|
117
|
+
return {"error": error_start}
|
|
118
|
+
|
|
119
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
120
|
+
if not is_valid_end:
|
|
121
|
+
return {"error": error_end}
|
|
122
|
+
|
|
123
|
+
# Validate date range
|
|
124
|
+
if billing_period_start > billing_period_end:
|
|
125
|
+
return {
|
|
126
|
+
"error": f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
if "Dimensions" in expression:
|
|
131
|
+
dimension = expression["Dimensions"]
|
|
132
|
+
if (
|
|
133
|
+
"Key" not in dimension
|
|
134
|
+
or "Values" not in dimension
|
|
135
|
+
or "MatchOptions" not in dimension
|
|
136
|
+
):
|
|
137
|
+
return {
|
|
138
|
+
"error": 'Dimensions filter must include "Key", "Values", and "MatchOptions".'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dimension_key = dimension["Key"]
|
|
142
|
+
dimension_values = dimension["Values"]
|
|
143
|
+
valid_values_response = get_dimension_values(
|
|
144
|
+
dimension_key, billing_period_start, billing_period_end
|
|
145
|
+
)
|
|
146
|
+
if "error" in valid_values_response:
|
|
147
|
+
return {"error": valid_values_response["error"]}
|
|
148
|
+
valid_values = valid_values_response["values"]
|
|
149
|
+
for value in dimension_values:
|
|
150
|
+
if value not in valid_values:
|
|
151
|
+
return {
|
|
152
|
+
"error": f"Invalid value '{value}' for dimension '{dimension_key}'. Valid values are: {valid_values}"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if "Tags" in expression:
|
|
156
|
+
tag = expression["Tags"]
|
|
157
|
+
if "Key" not in tag or "Values" not in tag or "MatchOptions" not in tag:
|
|
158
|
+
return {"error": 'Tags filter must include "Key", "Values", and "MatchOptions".'}
|
|
159
|
+
|
|
160
|
+
tag_key = tag["Key"]
|
|
161
|
+
tag_values = tag["Values"]
|
|
162
|
+
valid_tag_values_response = get_tag_values(
|
|
163
|
+
tag_key, billing_period_start, billing_period_end
|
|
164
|
+
)
|
|
165
|
+
if "error" in valid_tag_values_response:
|
|
166
|
+
return {"error": valid_tag_values_response["error"]}
|
|
167
|
+
valid_tag_values = valid_tag_values_response["values"]
|
|
168
|
+
for value in tag_values:
|
|
169
|
+
if value not in valid_tag_values:
|
|
170
|
+
return {
|
|
171
|
+
"error": f"Invalid value '{value}' for tag '{tag_key}'. Valid values are: {valid_tag_values}"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if "CostCategories" in expression:
|
|
175
|
+
cost_category = expression["CostCategories"]
|
|
176
|
+
if (
|
|
177
|
+
"Key" not in cost_category
|
|
178
|
+
or "Values" not in cost_category
|
|
179
|
+
or "MatchOptions" not in cost_category
|
|
180
|
+
):
|
|
181
|
+
return {
|
|
182
|
+
"error": 'CostCategories filter must include "Key", "Values", and "MatchOptions".'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
logical_operators = ["And", "Or", "Not"]
|
|
186
|
+
logical_count = sum(1 for op in logical_operators if op in expression)
|
|
187
|
+
|
|
188
|
+
if logical_count > 1:
|
|
189
|
+
return {
|
|
190
|
+
"error": "Only one logical operator (And, Or, Not) is allowed per expression in filter parameter."
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if logical_count == 0 and len(expression) > 1:
|
|
194
|
+
return {
|
|
195
|
+
"error": "Filter parameter with multiple expressions require a logical operator (And, Or, Not)."
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if "And" in expression:
|
|
199
|
+
if not isinstance(expression["And"], list):
|
|
200
|
+
return {"error": "And expression must be a list of expressions."}
|
|
201
|
+
for sub_expression in expression["And"]:
|
|
202
|
+
result = validate_expression(
|
|
203
|
+
sub_expression, billing_period_start, billing_period_end
|
|
204
|
+
)
|
|
205
|
+
if "error" in result:
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
if "Or" in expression:
|
|
209
|
+
if not isinstance(expression["Or"], list):
|
|
210
|
+
return {"error": "Or expression must be a list of expressions."}
|
|
211
|
+
for sub_expression in expression["Or"]:
|
|
212
|
+
result = validate_expression(
|
|
213
|
+
sub_expression, billing_period_start, billing_period_end
|
|
214
|
+
)
|
|
215
|
+
if "error" in result:
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
if "Not" in expression:
|
|
219
|
+
if not isinstance(expression["Not"], dict):
|
|
220
|
+
return {"error": "Not expression must be a single expression."}
|
|
221
|
+
result = validate_expression(
|
|
222
|
+
expression["Not"], billing_period_start, billing_period_end
|
|
223
|
+
)
|
|
224
|
+
if "error" in result:
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
if not any(
|
|
228
|
+
k in expression for k in ["Dimensions", "Tags", "CostCategories", "And", "Or", "Not"]
|
|
229
|
+
):
|
|
230
|
+
return {
|
|
231
|
+
"error": 'Filter Expression must include at least one of the following keys: "Dimensions", "Tags", "CostCategories", "And", "Or", "Not".'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {}
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return {"error": f"Error validating expression: {str(e)}"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def validate_group_by(group_by: Dict[str, Any]) -> Dict[str, Any]:
|
|
240
|
+
"""Validate the group_by parameter.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
group_by: The group_by dictionary to validate
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Empty dictionary if valid, or an error dictionary
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
if not isinstance(group_by, dict) or "Type" not in group_by or "Key" not in group_by:
|
|
250
|
+
return {"error": 'group_by must be a dictionary with "Type" and "Key" keys.'}
|
|
251
|
+
|
|
252
|
+
if group_by["Type"].upper() not in ["DIMENSION", "TAG", "COST_CATEGORY"]:
|
|
253
|
+
return {
|
|
254
|
+
"error": "Invalid group Type. Valid types are DIMENSION, TAG, and COST_CATEGORY."
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {}
|
|
258
|
+
except Exception as e:
|
|
259
|
+
return {"error": f"Error validating group_by: {str(e)}"}
|