awslabs.cdk-mcp-server 0.0.10417__py3-none-any.whl
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/__init__.py +2 -0
- awslabs/cdk_mcp_server/__init__.py +8 -0
- awslabs/cdk_mcp_server/core/__init__.py +1 -0
- awslabs/cdk_mcp_server/core/resources.py +271 -0
- awslabs/cdk_mcp_server/core/search_utils.py +182 -0
- awslabs/cdk_mcp_server/core/server.py +74 -0
- awslabs/cdk_mcp_server/core/tools.py +324 -0
- awslabs/cdk_mcp_server/data/__init__.py +1 -0
- awslabs/cdk_mcp_server/data/cdk_nag_parser.py +331 -0
- awslabs/cdk_mcp_server/data/construct_descriptions.py +32 -0
- awslabs/cdk_mcp_server/data/genai_cdk_loader.py +423 -0
- awslabs/cdk_mcp_server/data/lambda_powertools_loader.py +48 -0
- awslabs/cdk_mcp_server/data/schema_generator.py +666 -0
- awslabs/cdk_mcp_server/data/solutions_constructs_parser.py +782 -0
- awslabs/cdk_mcp_server/server.py +7 -0
- awslabs/cdk_mcp_server/static/CDK_GENERAL_GUIDANCE.md +232 -0
- awslabs/cdk_mcp_server/static/CDK_NAG_GUIDANCE.md +192 -0
- awslabs/cdk_mcp_server/static/__init__.py +5 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/actiongroups.md +137 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/alias.md +39 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/collaboration.md +91 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/creation.md +149 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/custom_orchestration.md +74 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/overview.md +78 -0
- awslabs/cdk_mcp_server/static/bedrock/agent/prompt_override.md +70 -0
- awslabs/cdk_mcp_server/static/bedrock/bedrockguardrails.md +188 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/chunking.md +137 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/datasources.md +225 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/kendra.md +81 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/overview.md +116 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/parsing.md +36 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/transformation.md +30 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/aurora.md +185 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/creation.md +80 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/opensearch.md +56 -0
- awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/pinecone.md +66 -0
- awslabs/cdk_mcp_server/static/bedrock/profiles.md +153 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/actiongroups.md +137 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/alias.md +39 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/collaboration.md +91 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/creation.md +149 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/custom_orchestration.md +74 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/overview.md +78 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/prompt_override.md +70 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/bedrockguardrails.md +188 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/chunking.md +137 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/datasources.md +225 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/kendra.md +81 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/overview.md +116 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/parsing.md +36 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/transformation.md +30 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/aurora.md +185 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/creation.md +80 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/opensearch.md +56 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/pinecone.md +66 -0
- awslabs/cdk_mcp_server/static/genai_cdk/bedrock/profiles.md +153 -0
- awslabs/cdk_mcp_server/static/genai_cdk/opensearch-vectorindex/overview.md +135 -0
- awslabs/cdk_mcp_server/static/genai_cdk/opensearchserverless/overview.md +17 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/bedrock.md +127 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/cdk.md +99 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/dependencies.md +45 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/index.md +36 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/insights.md +95 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/logging.md +43 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/metrics.md +93 -0
- awslabs/cdk_mcp_server/static/lambda_powertools/tracing.md +63 -0
- awslabs/cdk_mcp_server/static/opensearch-vectorindex/overview.md +135 -0
- awslabs/cdk_mcp_server/static/opensearchserverless/overview.md +17 -0
- awslabs_cdk_mcp_server-0.0.10417.dist-info/METADATA +14 -0
- awslabs_cdk_mcp_server-0.0.10417.dist-info/RECORD +72 -0
- awslabs_cdk_mcp_server-0.0.10417.dist-info/WHEEL +4 -0
- awslabs_cdk_mcp_server-0.0.10417.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
"""AWS Solutions Constructs patterns parser module."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from awslabs.cdk_mcp_server.core import search_utils
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Set up logging
|
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Constants
|
|
17
|
+
GITHUB_API_URL = 'https://api.github.com'
|
|
18
|
+
GITHUB_RAW_CONTENT_URL = 'https://raw.githubusercontent.com'
|
|
19
|
+
REPO_OWNER = 'awslabs'
|
|
20
|
+
REPO_NAME = 'aws-solutions-constructs'
|
|
21
|
+
PATTERNS_PATH = 'source/patterns/@aws-solutions-constructs'
|
|
22
|
+
CACHE_TTL = timedelta(hours=24) # Cache for 24 hours
|
|
23
|
+
|
|
24
|
+
# Cache for pattern list and pattern details
|
|
25
|
+
_pattern_list_cache = {'timestamp': None, 'data': []}
|
|
26
|
+
_pattern_details_cache = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def fetch_pattern_list() -> List[str]:
|
|
30
|
+
"""Fetch the list of available AWS Solutions Constructs patterns.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of pattern names (e.g., ['aws-lambda-dynamodb', 'aws-apigateway-lambda', ...])
|
|
34
|
+
"""
|
|
35
|
+
global _pattern_list_cache
|
|
36
|
+
|
|
37
|
+
# Initialize cache if it's None
|
|
38
|
+
if _pattern_list_cache is None:
|
|
39
|
+
_pattern_list_cache = {'timestamp': None, 'data': []}
|
|
40
|
+
|
|
41
|
+
# Check cache first
|
|
42
|
+
if (
|
|
43
|
+
_pattern_list_cache['timestamp'] is not None
|
|
44
|
+
and _pattern_list_cache['data'] is not None
|
|
45
|
+
and datetime.now() - _pattern_list_cache['timestamp'] < CACHE_TTL
|
|
46
|
+
):
|
|
47
|
+
return _pattern_list_cache['data']
|
|
48
|
+
|
|
49
|
+
# Fetch from GitHub API
|
|
50
|
+
async with httpx.AsyncClient() as client:
|
|
51
|
+
response = await client.get(
|
|
52
|
+
f'{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/contents/{PATTERNS_PATH}',
|
|
53
|
+
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if response.status_code != 200:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
content = response.json()
|
|
60
|
+
|
|
61
|
+
# Filter for directories that are actual patterns (exclude core, resources, etc.)
|
|
62
|
+
patterns = [
|
|
63
|
+
item['name']
|
|
64
|
+
for item in content
|
|
65
|
+
if item['type'] == 'dir' and item['name'].startswith('aws-')
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Update cache
|
|
69
|
+
_pattern_list_cache['timestamp'] = datetime.now()
|
|
70
|
+
_pattern_list_cache['data'] = patterns
|
|
71
|
+
|
|
72
|
+
return patterns
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def get_pattern_info(pattern_name: str) -> Dict[str, Any]:
|
|
76
|
+
"""Get metadata information about a specific pattern.
|
|
77
|
+
|
|
78
|
+
This function returns only metadata about the pattern, not the full documentation.
|
|
79
|
+
For complete documentation, use the resource URI: aws-solutions-constructs://{pattern_name}
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
pattern_name: Name of the pattern (e.g., 'aws-lambda-dynamodb')
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary with pattern metadata
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
logger.info(f'Fetching pattern info for {pattern_name}')
|
|
89
|
+
|
|
90
|
+
# Decode the pattern name if it's URL-encoded
|
|
91
|
+
pattern_name = urllib.parse.unquote(pattern_name)
|
|
92
|
+
|
|
93
|
+
# Check cache first
|
|
94
|
+
if (
|
|
95
|
+
pattern_name in _pattern_details_cache
|
|
96
|
+
and datetime.now() - _pattern_details_cache[pattern_name]['timestamp'] < CACHE_TTL
|
|
97
|
+
):
|
|
98
|
+
logger.info(f'Using cached info for {pattern_name}')
|
|
99
|
+
return _pattern_details_cache[pattern_name]['data']
|
|
100
|
+
|
|
101
|
+
# Fetch README.md content
|
|
102
|
+
async with httpx.AsyncClient() as client:
|
|
103
|
+
readme_url = f'{GITHUB_RAW_CONTENT_URL}/{REPO_OWNER}/{REPO_NAME}/main/{PATTERNS_PATH}/{pattern_name}/README.md'
|
|
104
|
+
logger.info(f'Fetching README from {readme_url}')
|
|
105
|
+
response = await client.get(readme_url)
|
|
106
|
+
|
|
107
|
+
if response.status_code != 200:
|
|
108
|
+
logger.warning(
|
|
109
|
+
f'Failed to fetch README for {pattern_name}: HTTP {response.status_code}'
|
|
110
|
+
)
|
|
111
|
+
return {
|
|
112
|
+
'error': f'Pattern {pattern_name} not found or README.md not available',
|
|
113
|
+
'status_code': response.status_code,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
readme_content = response.text
|
|
117
|
+
|
|
118
|
+
# Extract only metadata
|
|
119
|
+
services = extract_services_from_pattern_name(pattern_name)
|
|
120
|
+
description = extract_description(readme_content)
|
|
121
|
+
use_cases = extract_use_cases(readme_content)
|
|
122
|
+
|
|
123
|
+
# Create pattern info with only metadata
|
|
124
|
+
pattern_info = {
|
|
125
|
+
'pattern_name': pattern_name,
|
|
126
|
+
'services': services,
|
|
127
|
+
'description': description,
|
|
128
|
+
'use_cases': use_cases,
|
|
129
|
+
'documentation_uri': f'aws-solutions-constructs://{pattern_name}',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Update cache
|
|
133
|
+
global _pattern_details_cache
|
|
134
|
+
if _pattern_details_cache is None:
|
|
135
|
+
_pattern_details_cache = {}
|
|
136
|
+
|
|
137
|
+
_pattern_details_cache[pattern_name] = {'timestamp': datetime.now(), 'data': pattern_info}
|
|
138
|
+
|
|
139
|
+
return pattern_info
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f'Error processing pattern {pattern_name}: {str(e)}')
|
|
142
|
+
return {
|
|
143
|
+
'error': f'Error processing pattern {pattern_name}: {str(e)}',
|
|
144
|
+
'pattern_name': pattern_name,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def get_pattern_raw(pattern_name: str) -> Dict[str, Any]:
|
|
149
|
+
"""Get raw README.md content for a specific pattern.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
pattern_name: Name of the pattern (e.g., 'aws-lambda-dynamodb')
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dictionary with raw pattern documentation
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
logger.info(f'Fetching raw pattern info for {pattern_name}')
|
|
159
|
+
|
|
160
|
+
# Decode the pattern name if it's URL-encoded
|
|
161
|
+
pattern_name = urllib.parse.unquote(pattern_name)
|
|
162
|
+
|
|
163
|
+
# Fetch README.md content
|
|
164
|
+
async with httpx.AsyncClient() as client:
|
|
165
|
+
readme_url = f'{GITHUB_RAW_CONTENT_URL}/{REPO_OWNER}/{REPO_NAME}/main/{PATTERNS_PATH}/{pattern_name}/README.md'
|
|
166
|
+
logger.info(f'Fetching README from {readme_url}')
|
|
167
|
+
response = await client.get(readme_url)
|
|
168
|
+
|
|
169
|
+
if response.status_code != 200:
|
|
170
|
+
logger.warning(
|
|
171
|
+
f'Failed to fetch README for {pattern_name}: HTTP {response.status_code}'
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
'error': f'Pattern {pattern_name} not found or README.md not available',
|
|
175
|
+
'status_code': response.status_code,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
readme_content = response.text
|
|
179
|
+
|
|
180
|
+
# Extract services from pattern name
|
|
181
|
+
services = extract_services_from_pattern_name(pattern_name)
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
'status': 'success',
|
|
185
|
+
'pattern_name': pattern_name,
|
|
186
|
+
'services': services,
|
|
187
|
+
'content': readme_content,
|
|
188
|
+
'message': f'Retrieved pattern documentation for {pattern_name}',
|
|
189
|
+
}
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f'Error fetching raw pattern {pattern_name}: {str(e)}')
|
|
192
|
+
return {
|
|
193
|
+
'status': 'error',
|
|
194
|
+
'pattern_name': pattern_name,
|
|
195
|
+
'error': f'Error fetching pattern documentation: {str(e)}',
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_readme_content(pattern_name: str, content: str) -> Dict[str, Any]:
|
|
200
|
+
"""Parse README.md content to extract pattern information.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
pattern_name: Name of the pattern
|
|
204
|
+
content: README.md content
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Dictionary with parsed pattern information
|
|
208
|
+
"""
|
|
209
|
+
result = {
|
|
210
|
+
'pattern_name': pattern_name,
|
|
211
|
+
'services': extract_services_from_pattern_name(pattern_name),
|
|
212
|
+
'description': extract_description(content),
|
|
213
|
+
'props': extract_props(content),
|
|
214
|
+
'props_markdown': extract_props_markdown(content),
|
|
215
|
+
'properties': extract_properties(content),
|
|
216
|
+
'default_settings': extract_default_settings(content),
|
|
217
|
+
'code_example': extract_code_example(content),
|
|
218
|
+
'use_cases': extract_use_cases(content),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def extract_props_markdown(content: str) -> str:
|
|
225
|
+
"""Extract the Pattern Construct Props section as markdown from README.md content.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
content: README.md content
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Markdown string containing the Pattern Construct Props section
|
|
232
|
+
"""
|
|
233
|
+
# Look for the Pattern Construct Props section
|
|
234
|
+
props_section_match = re.search(
|
|
235
|
+
r'## Pattern Construct Props(.*?)(?=##|\Z)', content, re.DOTALL
|
|
236
|
+
)
|
|
237
|
+
if not props_section_match:
|
|
238
|
+
# Try alternative section names
|
|
239
|
+
props_section_match = re.search(r'## Construct Props(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
240
|
+
if not props_section_match:
|
|
241
|
+
props_section_match = re.search(r'## Props(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
242
|
+
if not props_section_match:
|
|
243
|
+
return 'No props section found'
|
|
244
|
+
|
|
245
|
+
# Return the entire section as markdown
|
|
246
|
+
return props_section_match.group(1).strip()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def extract_services_from_pattern_name(pattern_name: str) -> List[str]:
|
|
250
|
+
"""Extract AWS service names from the pattern name.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
pattern_name: Name of the pattern (e.g., 'aws-lambda-dynamodb')
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of service names (e.g., ['Lambda', 'DynamoDB'])
|
|
257
|
+
"""
|
|
258
|
+
# Remove 'aws-' prefix and split by '-'
|
|
259
|
+
parts = pattern_name[4:].split('-')
|
|
260
|
+
|
|
261
|
+
# Map to proper service names
|
|
262
|
+
service_mapping = {
|
|
263
|
+
'lambda': 'Lambda',
|
|
264
|
+
'dynamodb': 'DynamoDB',
|
|
265
|
+
'apigateway': 'API Gateway',
|
|
266
|
+
's3': 'S3',
|
|
267
|
+
'sqs': 'SQS',
|
|
268
|
+
'sns': 'SNS',
|
|
269
|
+
'eventbridge': 'EventBridge',
|
|
270
|
+
'kinesisfirehose': 'Kinesis Firehose',
|
|
271
|
+
'kinesisstreams': 'Kinesis Streams',
|
|
272
|
+
'cloudfront': 'CloudFront',
|
|
273
|
+
'alb': 'Application Load Balancer',
|
|
274
|
+
'fargate': 'Fargate',
|
|
275
|
+
'iot': 'IoT Core',
|
|
276
|
+
'elasticsearch': 'Elasticsearch',
|
|
277
|
+
'opensearch': 'OpenSearch',
|
|
278
|
+
'secretsmanager': 'Secrets Manager',
|
|
279
|
+
'sagemakerendpoint': 'SageMaker Endpoint',
|
|
280
|
+
'stepfunctions': 'Step Functions',
|
|
281
|
+
'wafwebacl': 'WAF Web ACL',
|
|
282
|
+
'cognito': 'Cognito',
|
|
283
|
+
'appsync': 'AppSync',
|
|
284
|
+
'kendra': 'Kendra',
|
|
285
|
+
'elasticachememcached': 'ElastiCache Memcached',
|
|
286
|
+
'ssmstringparameter': 'SSM String Parameter',
|
|
287
|
+
'mediastore': 'MediaStore',
|
|
288
|
+
'gluejob': 'Glue Job',
|
|
289
|
+
'pipes': 'EventBridge Pipes',
|
|
290
|
+
'oai': 'Origin Access Identity',
|
|
291
|
+
'route53': 'Route 53',
|
|
292
|
+
'openapigateway': 'API Gateway (OpenAPI)',
|
|
293
|
+
'apigatewayv2websocket': 'API Gateway v2 WebSocket',
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return [service_mapping.get(part, part.capitalize()) for part in parts]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def extract_description(content: str) -> str:
|
|
300
|
+
"""Extract the pattern description from README.md content.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
content: README.md content
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Pattern description
|
|
307
|
+
"""
|
|
308
|
+
# First, try to find a dedicated Description section
|
|
309
|
+
desc_section_match = re.search(r'## Description\s*\n+(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
310
|
+
if desc_section_match:
|
|
311
|
+
return desc_section_match.group(1).strip()
|
|
312
|
+
|
|
313
|
+
# Next, try to find an Overview section
|
|
314
|
+
overview_section_match = re.search(r'## Overview\s*\n+(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
315
|
+
if overview_section_match:
|
|
316
|
+
# Take the first paragraph of the overview
|
|
317
|
+
overview = overview_section_match.group(1).strip()
|
|
318
|
+
first_para_match = re.search(r'^(.*?)(?=\n\n|\Z)', overview, re.DOTALL)
|
|
319
|
+
if first_para_match:
|
|
320
|
+
return first_para_match.group(1).strip()
|
|
321
|
+
return overview
|
|
322
|
+
|
|
323
|
+
# Try to find the first paragraph after the title
|
|
324
|
+
match = re.search(r'# [^\n]*\n\n(.*?)(?=\n\n|\n##|\Z)', content, re.DOTALL)
|
|
325
|
+
if match:
|
|
326
|
+
return match.group(1).strip()
|
|
327
|
+
|
|
328
|
+
# Fallback: Try to find any text before the first ## heading
|
|
329
|
+
match = re.search(r'\n\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
330
|
+
if match:
|
|
331
|
+
return match.group(1).strip()
|
|
332
|
+
|
|
333
|
+
# If all else fails, extract the title as a fallback
|
|
334
|
+
title_match = re.search(r'# ([^\n]+)', content)
|
|
335
|
+
if title_match:
|
|
336
|
+
pattern_name = title_match.group(1).strip()
|
|
337
|
+
return f'A pattern for integrating {pattern_name} services'
|
|
338
|
+
|
|
339
|
+
return 'No description available'
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def extract_props(content: str) -> Dict[str, Dict[str, Any]]:
|
|
343
|
+
"""Extract pattern construct props from README.md content.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
content: README.md content
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Dictionary of props with their descriptions
|
|
350
|
+
"""
|
|
351
|
+
props = {}
|
|
352
|
+
|
|
353
|
+
# Look for the Pattern Construct Props section
|
|
354
|
+
props_section_match = re.search(
|
|
355
|
+
r'## Pattern Construct Props(.*?)(?=##|\Z)', content, re.DOTALL
|
|
356
|
+
)
|
|
357
|
+
if not props_section_match:
|
|
358
|
+
# Try alternative section names
|
|
359
|
+
props_section_match = re.search(r'## Construct Props(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
360
|
+
if not props_section_match:
|
|
361
|
+
props_section_match = re.search(r'## Props(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
362
|
+
if not props_section_match:
|
|
363
|
+
return props
|
|
364
|
+
|
|
365
|
+
props_section = props_section_match.group(1)
|
|
366
|
+
|
|
367
|
+
# First, try to find a markdown table with headers
|
|
368
|
+
# Look for a table with a header row and a separator row
|
|
369
|
+
table_match = re.search(
|
|
370
|
+
r'\|([^|]*\|)+\s*\n\s*\|([-:]+\|)+\s*\n(.*?)(?=\n\s*\n|\Z)', props_section, re.DOTALL
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if table_match:
|
|
374
|
+
table_content = table_match.group(3)
|
|
375
|
+
# Extract rows from the table
|
|
376
|
+
rows = re.finditer(r'\|\s*(?:`([^`]+)`|([^|]+))\s*\|(.*?)\|', table_content)
|
|
377
|
+
|
|
378
|
+
for row in rows:
|
|
379
|
+
# The prop name could be in backticks or not
|
|
380
|
+
prop_name = row.group(1) if row.group(1) else row.group(2).strip()
|
|
381
|
+
prop_desc = row.group(3).strip()
|
|
382
|
+
|
|
383
|
+
# Skip empty prop names or separator rows
|
|
384
|
+
if not prop_name or prop_name.startswith('-') or all(c in '-:|' for c in prop_name):
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
# Skip header rows
|
|
388
|
+
if prop_name.lower() in ['name', 'property', 'parameter', 'prop']:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
# Determine if required
|
|
392
|
+
required = (
|
|
393
|
+
'required' in prop_desc.lower()
|
|
394
|
+
and 'not required' not in prop_desc.lower()
|
|
395
|
+
and 'optional' not in prop_desc.lower()
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Try to determine type
|
|
399
|
+
type_match = re.search(r'([a-zA-Z0-9.]+(?:\.[a-zA-Z0-9]+)+)', prop_desc)
|
|
400
|
+
prop_type = type_match.group(1) if type_match else 'unknown'
|
|
401
|
+
|
|
402
|
+
# Look for default value
|
|
403
|
+
default_match = re.search(
|
|
404
|
+
r'Default(?:s)?\s*(?:is|:|to)?\s*[`"]?([^`"\n]+)[`"]?', prop_desc, re.IGNORECASE
|
|
405
|
+
)
|
|
406
|
+
default_value = default_match.group(1).strip() if default_match else None
|
|
407
|
+
|
|
408
|
+
props[prop_name] = {
|
|
409
|
+
'description': prop_desc,
|
|
410
|
+
'required': required,
|
|
411
|
+
'type': prop_type,
|
|
412
|
+
'default': default_value,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# If no table found or no props extracted from table, try to find prop definitions in other formats
|
|
416
|
+
if not props:
|
|
417
|
+
# Look for definitions like "- `propName`: Description"
|
|
418
|
+
prop_defs = re.finditer(
|
|
419
|
+
r'[-*]\s*`([^`]+)`\s*:\s*(.*?)(?=\n[-*]|\n##|\Z)', props_section, re.DOTALL
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
for prop_def in prop_defs:
|
|
423
|
+
prop_name = prop_def.group(1)
|
|
424
|
+
prop_desc = prop_def.group(2).strip()
|
|
425
|
+
|
|
426
|
+
# Determine if required
|
|
427
|
+
required = (
|
|
428
|
+
'required' in prop_desc.lower()
|
|
429
|
+
and 'not required' not in prop_desc.lower()
|
|
430
|
+
and 'optional' not in prop_desc.lower()
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Try to determine type
|
|
434
|
+
type_match = re.search(r'([a-zA-Z0-9.]+(?:\.[a-zA-Z0-9]+)+)', prop_desc)
|
|
435
|
+
prop_type = type_match.group(1) if type_match else 'unknown'
|
|
436
|
+
|
|
437
|
+
# Look for default value
|
|
438
|
+
default_match = re.search(
|
|
439
|
+
r'Default(?:s)?\s*(?:is|:|to)?\s*[`"]?([^`"\n]+)[`"]?', prop_desc, re.IGNORECASE
|
|
440
|
+
)
|
|
441
|
+
default_value = default_match.group(1).strip() if default_match else None
|
|
442
|
+
|
|
443
|
+
props[prop_name] = {
|
|
444
|
+
'description': prop_desc,
|
|
445
|
+
'required': required,
|
|
446
|
+
'type': prop_type,
|
|
447
|
+
'default': default_value,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# If still no props, try to find bullet points with prop descriptions
|
|
451
|
+
if not props:
|
|
452
|
+
# Look for bullet points with prop descriptions
|
|
453
|
+
bullet_props = re.finditer(r'[-*]\s*(.*?)(?=\n[-*]|\n##|\Z)', props_section, re.DOTALL)
|
|
454
|
+
|
|
455
|
+
for bullet_prop in bullet_props:
|
|
456
|
+
bullet_text = bullet_prop.group(1).strip()
|
|
457
|
+
|
|
458
|
+
# Try to extract prop name and description
|
|
459
|
+
prop_match = re.search(r'^([a-zA-Z0-9_]+)\s*[-:]\s*(.*)', bullet_text)
|
|
460
|
+
if prop_match:
|
|
461
|
+
prop_name = prop_match.group(1)
|
|
462
|
+
prop_desc = prop_match.group(2).strip()
|
|
463
|
+
|
|
464
|
+
# Determine if required
|
|
465
|
+
required = (
|
|
466
|
+
'required' in prop_desc.lower()
|
|
467
|
+
and 'not required' not in prop_desc.lower()
|
|
468
|
+
and 'optional' not in prop_desc.lower()
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Try to determine type
|
|
472
|
+
type_match = re.search(r'([a-zA-Z0-9.]+(?:\.[a-zA-Z0-9]+)+)', prop_desc)
|
|
473
|
+
prop_type = type_match.group(1) if type_match else 'unknown'
|
|
474
|
+
|
|
475
|
+
# Look for default value
|
|
476
|
+
default_match = re.search(
|
|
477
|
+
r'Default(?:s)?\s*(?:is|:|to)?\s*[`"]?([^`"\n]+)[`"]?',
|
|
478
|
+
prop_desc,
|
|
479
|
+
re.IGNORECASE,
|
|
480
|
+
)
|
|
481
|
+
default_value = default_match.group(1).strip() if default_match else None
|
|
482
|
+
|
|
483
|
+
props[prop_name] = {
|
|
484
|
+
'description': prop_desc,
|
|
485
|
+
'required': required,
|
|
486
|
+
'type': prop_type,
|
|
487
|
+
'default': default_value,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return props
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def extract_properties(content: str) -> Dict[str, Dict[str, Any]]:
|
|
494
|
+
"""Extract pattern properties from README.md content.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
content: README.md content
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Dictionary of properties with their descriptions
|
|
501
|
+
"""
|
|
502
|
+
properties = {}
|
|
503
|
+
|
|
504
|
+
# Look for the Pattern Properties section
|
|
505
|
+
props_section_match = re.search(r'## Pattern Properties(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
506
|
+
if not props_section_match:
|
|
507
|
+
return properties
|
|
508
|
+
|
|
509
|
+
props_section = props_section_match.group(1)
|
|
510
|
+
|
|
511
|
+
# Extract properties from the section
|
|
512
|
+
prop_matches = re.finditer(r'\|\s*`([^`]+)`\s*\|(.*?)\|', props_section)
|
|
513
|
+
|
|
514
|
+
for match in prop_matches:
|
|
515
|
+
prop_name = match.group(1)
|
|
516
|
+
prop_desc = match.group(2).strip()
|
|
517
|
+
|
|
518
|
+
# Try to determine type
|
|
519
|
+
type_match = re.search(r'([a-zA-Z0-9.]+(?:\.[a-zA-Z0-9]+)+)', prop_desc)
|
|
520
|
+
prop_type = type_match.group(1) if type_match else 'unknown'
|
|
521
|
+
|
|
522
|
+
# Look for access method
|
|
523
|
+
access_match = re.search(
|
|
524
|
+
r'(?:access|get|retrieve)(?:ed)?\s+(?:via|using|with|by)?\s+`([^`]+)`',
|
|
525
|
+
prop_desc,
|
|
526
|
+
re.IGNORECASE,
|
|
527
|
+
)
|
|
528
|
+
access_method = access_match.group(1) if access_match else None
|
|
529
|
+
|
|
530
|
+
properties[prop_name] = {
|
|
531
|
+
'description': prop_desc,
|
|
532
|
+
'type': prop_type,
|
|
533
|
+
'access_method': access_method,
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return properties
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def extract_default_settings(content: str) -> List[str]:
|
|
540
|
+
"""Extract default settings from README.md content.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
content: README.md content
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
List of default settings
|
|
547
|
+
"""
|
|
548
|
+
defaults = []
|
|
549
|
+
|
|
550
|
+
# Look for the Default Settings section
|
|
551
|
+
default_section_match = re.search(r'## Default Settings(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
552
|
+
if not default_section_match:
|
|
553
|
+
return defaults
|
|
554
|
+
|
|
555
|
+
default_section = default_section_match.group(1)
|
|
556
|
+
|
|
557
|
+
# Extract bullet points - handle both * and - style bullets
|
|
558
|
+
bullet_matches = re.finditer(
|
|
559
|
+
r'(?:\*|\-)\s*(.*?)(?=\n(?:\*|\-)|\n##|\n$|\Z)', default_section, re.DOTALL
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
for match in bullet_matches:
|
|
563
|
+
# Clean up any newlines or extra whitespace
|
|
564
|
+
setting = re.sub(r'\s+', ' ', match.group(1).strip())
|
|
565
|
+
defaults.append(setting)
|
|
566
|
+
|
|
567
|
+
return defaults
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def extract_code_example(content: str) -> str:
|
|
571
|
+
"""Extract a code example from README.md content.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
content: README.md content
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Code example as a string
|
|
578
|
+
"""
|
|
579
|
+
# First, look for TypeScript code blocks in the Architecture section
|
|
580
|
+
architecture_section_match = re.search(r'## Architecture(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
581
|
+
if architecture_section_match:
|
|
582
|
+
architecture_section = architecture_section_match.group(1)
|
|
583
|
+
code_match = re.search(r'```typescript\n(.*?)\n```', architecture_section, re.DOTALL)
|
|
584
|
+
if code_match:
|
|
585
|
+
return code_match.group(1).strip()
|
|
586
|
+
|
|
587
|
+
# Next, look for TypeScript code blocks in the entire content
|
|
588
|
+
code_match = re.search(r'```typescript\n(.*?)\n```', content, re.DOTALL)
|
|
589
|
+
if code_match:
|
|
590
|
+
return code_match.group(1).strip()
|
|
591
|
+
|
|
592
|
+
# Try JavaScript code blocks
|
|
593
|
+
code_match = re.search(r'```javascript\n(.*?)\n```', content, re.DOTALL)
|
|
594
|
+
if code_match:
|
|
595
|
+
return code_match.group(1).strip()
|
|
596
|
+
|
|
597
|
+
# Try Python code blocks
|
|
598
|
+
code_match = re.search(r'```python\n(.*?)\n```', content, re.DOTALL)
|
|
599
|
+
if code_match:
|
|
600
|
+
return code_match.group(1).strip()
|
|
601
|
+
|
|
602
|
+
# Try without language specifier
|
|
603
|
+
code_match = re.search(r'```\n(.*?)\n```', content, re.DOTALL)
|
|
604
|
+
if code_match:
|
|
605
|
+
return code_match.group(1).strip()
|
|
606
|
+
|
|
607
|
+
# Look for code blocks with indentation (4 spaces)
|
|
608
|
+
code_blocks = re.findall(r'(?:^|\n)( {4}[^\n]+(?:\n {4}[^\n]+)*)', content)
|
|
609
|
+
if code_blocks:
|
|
610
|
+
# Return the longest code block (most likely to be a complete example)
|
|
611
|
+
longest_block = max(code_blocks, key=len)
|
|
612
|
+
# Remove the 4-space indentation from each line
|
|
613
|
+
return '\n'.join(line[4:] for line in longest_block.split('\n'))
|
|
614
|
+
|
|
615
|
+
return 'No code example available'
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def extract_use_cases(content: str) -> List[str]:
|
|
619
|
+
"""Extract use cases from README.md content.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
content: README.md content
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
List of use cases
|
|
626
|
+
"""
|
|
627
|
+
use_cases = []
|
|
628
|
+
|
|
629
|
+
# First, look for a dedicated Use Cases section
|
|
630
|
+
use_cases_section_match = re.search(r'## Use Cases(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
631
|
+
if use_cases_section_match:
|
|
632
|
+
use_cases_section = use_cases_section_match.group(1)
|
|
633
|
+
|
|
634
|
+
# Extract bullet points
|
|
635
|
+
bullet_matches = re.finditer(
|
|
636
|
+
r'(?:\*|\-)\s*(.*?)(?=\n(?:\*|\-)|\n##|\n$|\Z)', use_cases_section, re.DOTALL
|
|
637
|
+
)
|
|
638
|
+
for match in bullet_matches:
|
|
639
|
+
# Clean up any newlines or extra whitespace
|
|
640
|
+
use_case = re.sub(r'\s+', ' ', match.group(1).strip())
|
|
641
|
+
use_cases.append(use_case)
|
|
642
|
+
|
|
643
|
+
if use_cases:
|
|
644
|
+
return use_cases
|
|
645
|
+
|
|
646
|
+
# If no dedicated section, look for the Overview section
|
|
647
|
+
overview_match = re.search(r'## Overview(.*?)(?=##|\Z)', content, re.DOTALL)
|
|
648
|
+
if overview_match:
|
|
649
|
+
overview = overview_match.group(1)
|
|
650
|
+
|
|
651
|
+
# Look for sentences that might indicate use cases
|
|
652
|
+
sentences = re.split(r'(?<=[.!?])\s+', overview)
|
|
653
|
+
for sentence in sentences:
|
|
654
|
+
if any(
|
|
655
|
+
keyword in sentence.lower()
|
|
656
|
+
for keyword in [
|
|
657
|
+
'use',
|
|
658
|
+
'scenario',
|
|
659
|
+
'when',
|
|
660
|
+
'ideal',
|
|
661
|
+
'perfect',
|
|
662
|
+
'suitable',
|
|
663
|
+
'designed for',
|
|
664
|
+
]
|
|
665
|
+
):
|
|
666
|
+
use_cases.append(sentence.strip())
|
|
667
|
+
|
|
668
|
+
# Also check the main description for use case hints
|
|
669
|
+
description = extract_description(content)
|
|
670
|
+
if description != 'No description available':
|
|
671
|
+
sentences = re.split(r'(?<=[.!?])\s+', description)
|
|
672
|
+
for sentence in sentences:
|
|
673
|
+
if any(
|
|
674
|
+
keyword in sentence.lower()
|
|
675
|
+
for keyword in [
|
|
676
|
+
'use',
|
|
677
|
+
'scenario',
|
|
678
|
+
'when',
|
|
679
|
+
'ideal',
|
|
680
|
+
'perfect',
|
|
681
|
+
'suitable',
|
|
682
|
+
'designed for',
|
|
683
|
+
]
|
|
684
|
+
):
|
|
685
|
+
# Avoid duplicates
|
|
686
|
+
if sentence.strip() not in use_cases:
|
|
687
|
+
use_cases.append(sentence.strip())
|
|
688
|
+
|
|
689
|
+
# If we still couldn't find any, add a generic one based on the services
|
|
690
|
+
if not use_cases:
|
|
691
|
+
if description != 'No description available':
|
|
692
|
+
use_cases.append(f'Implementing {description}')
|
|
693
|
+
else:
|
|
694
|
+
services = extract_services_from_pattern_name(content.split('\n')[0].strip('# '))
|
|
695
|
+
use_cases.append(f'Integrating {" and ".join(services)}')
|
|
696
|
+
|
|
697
|
+
return use_cases
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
async def search_patterns(services: List[str]) -> List[Dict[str, Any]]:
|
|
701
|
+
"""Search for patterns that use specific AWS services.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
services: List of AWS service names to search for
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
List of matching patterns with their information
|
|
708
|
+
"""
|
|
709
|
+
try:
|
|
710
|
+
logger.info(f'Searching for patterns with services: {services}')
|
|
711
|
+
|
|
712
|
+
# Get all patterns
|
|
713
|
+
all_patterns = await fetch_pattern_list()
|
|
714
|
+
|
|
715
|
+
# Define functions to extract searchable text and name parts
|
|
716
|
+
def get_text_fn(pattern_name: str) -> str:
|
|
717
|
+
# Extract services from pattern name
|
|
718
|
+
services = extract_services_from_pattern_name(pattern_name)
|
|
719
|
+
return ' '.join(services).lower()
|
|
720
|
+
|
|
721
|
+
def get_name_parts_fn(pattern_name: str) -> List[str]:
|
|
722
|
+
return extract_services_from_pattern_name(pattern_name)
|
|
723
|
+
|
|
724
|
+
# Use common search utility
|
|
725
|
+
scored_patterns = search_utils.search_items_with_terms(
|
|
726
|
+
all_patterns, services, get_text_fn, get_name_parts_fn
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# Fetch full pattern info for matched patterns
|
|
730
|
+
matching_patterns = []
|
|
731
|
+
for scored_pattern in scored_patterns:
|
|
732
|
+
pattern_name = scored_pattern['item']
|
|
733
|
+
pattern_info = await get_pattern_info(pattern_name)
|
|
734
|
+
|
|
735
|
+
# Add matched terms to the result
|
|
736
|
+
pattern_info['matched_services'] = scored_pattern['matched_terms']
|
|
737
|
+
|
|
738
|
+
# Remove verbose use_cases field
|
|
739
|
+
if 'use_cases' in pattern_info:
|
|
740
|
+
del pattern_info['use_cases']
|
|
741
|
+
|
|
742
|
+
matching_patterns.append(pattern_info)
|
|
743
|
+
|
|
744
|
+
logger.info(f'Found {len(matching_patterns)} matching patterns')
|
|
745
|
+
return matching_patterns
|
|
746
|
+
except Exception as e:
|
|
747
|
+
logger.error(f'Error searching patterns: {str(e)}')
|
|
748
|
+
return []
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
async def get_all_patterns_info() -> List[Dict[str, Any]]:
|
|
752
|
+
"""Get information about all available patterns.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
List of pattern information dictionaries
|
|
756
|
+
"""
|
|
757
|
+
try:
|
|
758
|
+
logger.info('Fetching information for all patterns')
|
|
759
|
+
|
|
760
|
+
patterns = await fetch_pattern_list()
|
|
761
|
+
result = []
|
|
762
|
+
|
|
763
|
+
for pattern in patterns:
|
|
764
|
+
try:
|
|
765
|
+
pattern_info = await get_pattern_info(pattern)
|
|
766
|
+
result.append(pattern_info)
|
|
767
|
+
except Exception as e:
|
|
768
|
+
logger.error(f'Error fetching info for pattern {pattern}: {str(e)}')
|
|
769
|
+
# Add a minimal error entry so we don't lose the pattern in the list
|
|
770
|
+
result.append(
|
|
771
|
+
{
|
|
772
|
+
'pattern_name': pattern,
|
|
773
|
+
'error': f'Failed to fetch pattern info: {str(e)}',
|
|
774
|
+
'services': extract_services_from_pattern_name(pattern),
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
logger.info(f'Fetched information for {len(result)} patterns')
|
|
779
|
+
return result
|
|
780
|
+
except Exception as e:
|
|
781
|
+
logger.error(f'Error fetching all patterns info: {str(e)}')
|
|
782
|
+
return []
|