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.
Files changed (72) hide show
  1. awslabs/__init__.py +2 -0
  2. awslabs/cdk_mcp_server/__init__.py +8 -0
  3. awslabs/cdk_mcp_server/core/__init__.py +1 -0
  4. awslabs/cdk_mcp_server/core/resources.py +271 -0
  5. awslabs/cdk_mcp_server/core/search_utils.py +182 -0
  6. awslabs/cdk_mcp_server/core/server.py +74 -0
  7. awslabs/cdk_mcp_server/core/tools.py +324 -0
  8. awslabs/cdk_mcp_server/data/__init__.py +1 -0
  9. awslabs/cdk_mcp_server/data/cdk_nag_parser.py +331 -0
  10. awslabs/cdk_mcp_server/data/construct_descriptions.py +32 -0
  11. awslabs/cdk_mcp_server/data/genai_cdk_loader.py +423 -0
  12. awslabs/cdk_mcp_server/data/lambda_powertools_loader.py +48 -0
  13. awslabs/cdk_mcp_server/data/schema_generator.py +666 -0
  14. awslabs/cdk_mcp_server/data/solutions_constructs_parser.py +782 -0
  15. awslabs/cdk_mcp_server/server.py +7 -0
  16. awslabs/cdk_mcp_server/static/CDK_GENERAL_GUIDANCE.md +232 -0
  17. awslabs/cdk_mcp_server/static/CDK_NAG_GUIDANCE.md +192 -0
  18. awslabs/cdk_mcp_server/static/__init__.py +5 -0
  19. awslabs/cdk_mcp_server/static/bedrock/agent/actiongroups.md +137 -0
  20. awslabs/cdk_mcp_server/static/bedrock/agent/alias.md +39 -0
  21. awslabs/cdk_mcp_server/static/bedrock/agent/collaboration.md +91 -0
  22. awslabs/cdk_mcp_server/static/bedrock/agent/creation.md +149 -0
  23. awslabs/cdk_mcp_server/static/bedrock/agent/custom_orchestration.md +74 -0
  24. awslabs/cdk_mcp_server/static/bedrock/agent/overview.md +78 -0
  25. awslabs/cdk_mcp_server/static/bedrock/agent/prompt_override.md +70 -0
  26. awslabs/cdk_mcp_server/static/bedrock/bedrockguardrails.md +188 -0
  27. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/chunking.md +137 -0
  28. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/datasources.md +225 -0
  29. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/kendra.md +81 -0
  30. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/overview.md +116 -0
  31. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/parsing.md +36 -0
  32. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/transformation.md +30 -0
  33. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/aurora.md +185 -0
  34. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/creation.md +80 -0
  35. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/opensearch.md +56 -0
  36. awslabs/cdk_mcp_server/static/bedrock/knowledgebases/vector/pinecone.md +66 -0
  37. awslabs/cdk_mcp_server/static/bedrock/profiles.md +153 -0
  38. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/actiongroups.md +137 -0
  39. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/alias.md +39 -0
  40. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/collaboration.md +91 -0
  41. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/creation.md +149 -0
  42. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/custom_orchestration.md +74 -0
  43. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/overview.md +78 -0
  44. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/prompt_override.md +70 -0
  45. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/bedrockguardrails.md +188 -0
  46. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/chunking.md +137 -0
  47. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/datasources.md +225 -0
  48. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/kendra.md +81 -0
  49. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/overview.md +116 -0
  50. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/parsing.md +36 -0
  51. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/transformation.md +30 -0
  52. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/aurora.md +185 -0
  53. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/creation.md +80 -0
  54. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/opensearch.md +56 -0
  55. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/pinecone.md +66 -0
  56. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/profiles.md +153 -0
  57. awslabs/cdk_mcp_server/static/genai_cdk/opensearch-vectorindex/overview.md +135 -0
  58. awslabs/cdk_mcp_server/static/genai_cdk/opensearchserverless/overview.md +17 -0
  59. awslabs/cdk_mcp_server/static/lambda_powertools/bedrock.md +127 -0
  60. awslabs/cdk_mcp_server/static/lambda_powertools/cdk.md +99 -0
  61. awslabs/cdk_mcp_server/static/lambda_powertools/dependencies.md +45 -0
  62. awslabs/cdk_mcp_server/static/lambda_powertools/index.md +36 -0
  63. awslabs/cdk_mcp_server/static/lambda_powertools/insights.md +95 -0
  64. awslabs/cdk_mcp_server/static/lambda_powertools/logging.md +43 -0
  65. awslabs/cdk_mcp_server/static/lambda_powertools/metrics.md +93 -0
  66. awslabs/cdk_mcp_server/static/lambda_powertools/tracing.md +63 -0
  67. awslabs/cdk_mcp_server/static/opensearch-vectorindex/overview.md +135 -0
  68. awslabs/cdk_mcp_server/static/opensearchserverless/overview.md +17 -0
  69. awslabs_cdk_mcp_server-0.0.10417.dist-info/METADATA +14 -0
  70. awslabs_cdk_mcp_server-0.0.10417.dist-info/RECORD +72 -0
  71. awslabs_cdk_mcp_server-0.0.10417.dist-info/WHEEL +4 -0
  72. 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 []