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