awslabs.cost-explorer-mcp-server 0.0.2__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.2 → awslabs_cost_explorer_mcp_server-0.0.4}/Dockerfile +9 -6
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/PKG-INFO +3 -2
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/README.md +1 -0
- 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.4/awslabs/cost_explorer_mcp_server/helpers.py +278 -0
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/awslabs/cost_explorer_mcp_server/server.py +120 -101
- awslabs_cost_explorer_mcp_server-0.0.4/docker-healthcheck.sh +26 -0
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/pyproject.toml +24 -40
- 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.2 → awslabs_cost_explorer_mcp_server-0.0.4}/uv.lock +368 -184
- awslabs_cost_explorer_mcp_server-0.0.2/.pre-commit-config.yaml +0 -19
- awslabs_cost_explorer_mcp_server-0.0.2/awslabs/__init__.py +0 -3
- awslabs_cost_explorer_mcp_server-0.0.2/awslabs/cost_explorer_mcp_server/__init__.py +0 -6
- awslabs_cost_explorer_mcp_server-0.0.2/awslabs/cost_explorer_mcp_server/helpers.py +0 -259
- awslabs_cost_explorer_mcp_server-0.0.2/docker-healthcheck.sh +0 -12
- awslabs_cost_explorer_mcp_server-0.0.2/tests/__init__.py +0 -1
- awslabs_cost_explorer_mcp_server-0.0.2/tests/conftest.py +0 -93
- awslabs_cost_explorer_mcp_server-0.0.2/tests/test_helpers.py +0 -282
- awslabs_cost_explorer_mcp_server-0.0.2/tests/test_server.py +0 -427
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/.gitignore +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/.python-version +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/CHANGELOG.md +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/LICENSE +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.2 → 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]
|
{awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/Dockerfile
RENAMED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
4
|
-
#
|
|
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
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
8
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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.
|
|
11
14
|
|
|
12
15
|
FROM public.ecr.aws/sam/build-python3.10@sha256:e78695db10ca8cb129e59e30f7dc9789b0dbd0181dba195d68419c72bac51ac1 AS uv
|
|
13
16
|
|
|
@@ -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,7 @@ 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
|
+
|
|
130
131
|
## Cost Considerations
|
|
131
132
|
|
|
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.
|
|
@@ -98,6 +98,7 @@ 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
|
+
|
|
101
102
|
## Cost Considerations
|
|
102
103
|
|
|
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.
|
|
@@ -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'
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
"""Helper functions for the Cost Explorer MCP server."""
|
|
16
|
+
|
|
17
|
+
import boto3
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Any, Dict, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Set up logging
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Initialize AWS Cost Explorer client
|
|
28
|
+
ce = boto3.client('ce')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_date_format(date_str: str) -> Tuple[bool, str]:
|
|
32
|
+
"""Validate that a date string is in YYYY-MM-DD format and is a valid date.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
date_str: The date string to validate
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (is_valid, error_message)
|
|
39
|
+
"""
|
|
40
|
+
# Check format with regex
|
|
41
|
+
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
42
|
+
return False, f"Date '{date_str}' is not in YYYY-MM-DD format"
|
|
43
|
+
|
|
44
|
+
# Check if it's a valid date
|
|
45
|
+
try:
|
|
46
|
+
datetime.strptime(date_str, '%Y-%m-%d')
|
|
47
|
+
return True, ''
|
|
48
|
+
except ValueError as e:
|
|
49
|
+
return False, f"Invalid date '{date_str}': {str(e)}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_dimension_values(
|
|
53
|
+
key: str, billing_period_start: str, billing_period_end: str
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""Get available values for a specific dimension."""
|
|
56
|
+
# Validate date formats
|
|
57
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
58
|
+
if not is_valid_start:
|
|
59
|
+
return {'error': error_start}
|
|
60
|
+
|
|
61
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
62
|
+
if not is_valid_end:
|
|
63
|
+
return {'error': error_end}
|
|
64
|
+
|
|
65
|
+
# Validate date range
|
|
66
|
+
if billing_period_start > billing_period_end:
|
|
67
|
+
return {
|
|
68
|
+
'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
response = ce.get_dimension_values(
|
|
73
|
+
TimePeriod={'Start': billing_period_start, 'End': billing_period_end},
|
|
74
|
+
Dimension=key.upper(),
|
|
75
|
+
)
|
|
76
|
+
dimension_values = response['DimensionValues']
|
|
77
|
+
values = [value['Value'] for value in dimension_values]
|
|
78
|
+
return {'dimension': key.upper(), 'values': values}
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f'Error getting dimension values: {e}')
|
|
81
|
+
return {'error': str(e)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_tag_values(
|
|
85
|
+
tag_key: str, billing_period_start: str, billing_period_end: str
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
"""Get available values for a specific tag key."""
|
|
88
|
+
# Validate date formats
|
|
89
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
90
|
+
if not is_valid_start:
|
|
91
|
+
return {'error': error_start}
|
|
92
|
+
|
|
93
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
94
|
+
if not is_valid_end:
|
|
95
|
+
return {'error': error_end}
|
|
96
|
+
|
|
97
|
+
# Validate date range
|
|
98
|
+
if billing_period_start > billing_period_end:
|
|
99
|
+
return {
|
|
100
|
+
'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
response = ce.get_tags(
|
|
105
|
+
TimePeriod={'Start': billing_period_start, 'End': billing_period_end},
|
|
106
|
+
TagKey=tag_key,
|
|
107
|
+
)
|
|
108
|
+
tag_values = response['Tags']
|
|
109
|
+
return {'tag_key': tag_key, 'values': tag_values}
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f'Error getting tag values: {e}')
|
|
112
|
+
return {'error': str(e)}
|
|
113
|
+
|
|
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.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
expression: The filter expression to validate
|
|
122
|
+
billing_period_start: Start date of the billing period
|
|
123
|
+
billing_period_end: End date of the billing period
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Empty dictionary if valid, or an error dictionary
|
|
127
|
+
"""
|
|
128
|
+
# Validate date formats
|
|
129
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
130
|
+
if not is_valid_start:
|
|
131
|
+
return {'error': error_start}
|
|
132
|
+
|
|
133
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
134
|
+
if not is_valid_end:
|
|
135
|
+
return {'error': error_end}
|
|
136
|
+
|
|
137
|
+
# Validate date range
|
|
138
|
+
if billing_period_start > billing_period_end:
|
|
139
|
+
return {
|
|
140
|
+
'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
if 'Dimensions' in expression:
|
|
145
|
+
dimension = expression['Dimensions']
|
|
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
|
+
}
|
|
154
|
+
|
|
155
|
+
dimension_key = dimension['Key']
|
|
156
|
+
dimension_values = dimension['Values']
|
|
157
|
+
valid_values_response = get_dimension_values(
|
|
158
|
+
dimension_key, billing_period_start, billing_period_end
|
|
159
|
+
)
|
|
160
|
+
if 'error' in valid_values_response:
|
|
161
|
+
return {'error': valid_values_response['error']}
|
|
162
|
+
valid_values = valid_values_response['values']
|
|
163
|
+
for value in dimension_values:
|
|
164
|
+
if value not in valid_values:
|
|
165
|
+
return {
|
|
166
|
+
'error': f"Invalid value '{value}' for dimension '{dimension_key}'. Valid values are: {valid_values}"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if 'Tags' in expression:
|
|
170
|
+
tag = expression['Tags']
|
|
171
|
+
if 'Key' not in tag or 'Values' not in tag or 'MatchOptions' not in tag:
|
|
172
|
+
return {'error': 'Tags filter must include "Key", "Values", and "MatchOptions".'}
|
|
173
|
+
|
|
174
|
+
tag_key = tag['Key']
|
|
175
|
+
tag_values = tag['Values']
|
|
176
|
+
valid_tag_values_response = get_tag_values(
|
|
177
|
+
tag_key, billing_period_start, billing_period_end
|
|
178
|
+
)
|
|
179
|
+
if 'error' in valid_tag_values_response:
|
|
180
|
+
return {'error': valid_tag_values_response['error']}
|
|
181
|
+
valid_tag_values = valid_tag_values_response['values']
|
|
182
|
+
for value in tag_values:
|
|
183
|
+
if value not in valid_tag_values:
|
|
184
|
+
return {
|
|
185
|
+
'error': f"Invalid value '{value}' for tag '{tag_key}'. Valid values are: {valid_tag_values}"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if 'CostCategories' in expression:
|
|
189
|
+
cost_category = expression['CostCategories']
|
|
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
|
+
}
|
|
198
|
+
|
|
199
|
+
logical_operators = ['And', 'Or', 'Not']
|
|
200
|
+
logical_count = sum(1 for op in logical_operators if op in expression)
|
|
201
|
+
|
|
202
|
+
if logical_count > 1:
|
|
203
|
+
return {
|
|
204
|
+
'error': 'Only one logical operator (And, Or, Not) is allowed per expression in filter parameter.'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if logical_count == 0 and len(expression) > 1:
|
|
208
|
+
return {
|
|
209
|
+
'error': 'Filter parameter with multiple expressions require a logical operator (And, Or, Not).'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if 'And' in expression:
|
|
213
|
+
if not isinstance(expression['And'], list):
|
|
214
|
+
return {'error': 'And expression must be a list of expressions.'}
|
|
215
|
+
for sub_expression in expression['And']:
|
|
216
|
+
result = validate_expression(
|
|
217
|
+
sub_expression, billing_period_start, billing_period_end
|
|
218
|
+
)
|
|
219
|
+
if 'error' in result:
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
if 'Or' in expression:
|
|
223
|
+
if not isinstance(expression['Or'], list):
|
|
224
|
+
return {'error': 'Or expression must be a list of expressions.'}
|
|
225
|
+
for sub_expression in expression['Or']:
|
|
226
|
+
result = validate_expression(
|
|
227
|
+
sub_expression, billing_period_start, billing_period_end
|
|
228
|
+
)
|
|
229
|
+
if 'error' in result:
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
if 'Not' in expression:
|
|
233
|
+
if not isinstance(expression['Not'], dict):
|
|
234
|
+
return {'error': 'Not expression must be a single expression.'}
|
|
235
|
+
result = validate_expression(
|
|
236
|
+
expression['Not'], billing_period_start, billing_period_end
|
|
237
|
+
)
|
|
238
|
+
if 'error' in result:
|
|
239
|
+
return result
|
|
240
|
+
|
|
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
|
+
}
|
|
247
|
+
|
|
248
|
+
return {}
|
|
249
|
+
except Exception as e:
|
|
250
|
+
return {'error': f'Error validating expression: {str(e)}'}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def validate_group_by(group_by: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
254
|
+
"""Validate the group_by parameter.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
group_by: The group_by dictionary to validate
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Empty dictionary if valid, or an error dictionary
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
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
|
+
):
|
|
269
|
+
return {'error': 'group_by must be a dictionary with "Type" and "Key" keys.'}
|
|
270
|
+
|
|
271
|
+
if group_by['Type'].upper() not in ['DIMENSION', 'TAG', 'COST_CATEGORY']:
|
|
272
|
+
return {
|
|
273
|
+
'error': 'Invalid group Type. Valid types are DIMENSION, TAG, and COST_CATEGORY.'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {}
|
|
277
|
+
except Exception as e:
|
|
278
|
+
return {'error': f'Error validating group_by: {str(e)}'}
|