steering_docs/python-tech/feature_scenario.md
Generate feature scenarios that demonstrate complete workflows using multiple service operations in a guided, educational manner. Implementation must be based on the service SPECIFICATION.md file.
IMPORTANT: All new feature scenarios MUST be created in the python/example_code/{service}/scenarios/ directory.
python/example_code/{service}/scenarios/{scenario_name}/scenarios/features/{service_feature}/SPECIFICATION.mdMUST USE: All user interactions should use the standard functions in demo_tools.question. These functions provide consistent validation and error handling.
import demo_tools.question as q
# Basic question with no validation
answer = q.ask("Enter your name: ")
# Yes/No questions
use_existing = q.ask("Use existing resource? (y/n): ", q.is_yesno)
# Text input with validation
resource_name = q.ask("Enter resource name: ", q.non_empty)
# Integer input with validation
count = q.ask("How many items? ", q.is_int)
# Integer input with range validation
priority = q.ask("Enter priority (1-10): ", q.is_int, q.in_range(1, 10))
# Float input
price = q.ask("Enter price: ", q.is_float)
# Single letter input
option = q.ask("Select option (a-z): ", q.is_letter)
# Regular expression validation
email = q.ask("Enter email: ", q.re_match(r'^[^@]+@[^@]+\.[^@]+$'))
# Multiple choice selection
choices = ["Option A", "Option B", "Option C"]
selection = q.choose("Select an option: ", choices)
print(f"You selected: {choices[selection]}")
q.non_empty - Validates that the answer is not emptyq.is_yesno - Validates yes/no answers (accepts 'y' or 'Y' as True)q.is_int - Validates integer inputq.is_float - Validates floating-point inputq.is_letter - Validates single letter input (converts to uppercase)q.in_range(lower, upper) - Validates numeric input within a rangeq.re_match(pattern) - Validates input against a regular expression patternYou can chain multiple validators for complex validation:
# Integer between 1 and 100
value = q.ask("Enter value (1-100): ", q.is_int, q.in_range(1, 100))
# Non-empty string matching pattern
code = q.ask("Enter code: ", q.non_empty, q.re_match(r'^[A-Z]{3}-\d{3}$'))
# Progress indicators
print("✓ Operation completed successfully")
print("⚠ Warning: Resource may take time to initialize")
print("✗ Error occurred during operation")
# Section separators
print("-" * 80)
print("=" * 80)
# Formatted output
print(f"Found {len(items)} items:")
for i, item in enumerate(items, 1):
print(f" {i}. {item}")
# Wait for user acknowledgment
q.ask("\nPress Enter to continue...")
Feature scenarios use a structured approach with separate modules for scenarios, wrappers, and CloudFormation helpers:
python/example_code/{service}/scenarios/{scenario_name}/
├── {scenario_name}_scenario.py # Main scenario file
├── {service}_wrapper.py # Wrapper class for service operations
├── cloudformation_helper.py # CloudFormation deployment helper (if needed)
├── README.md # Scenario documentation
├── requirements.txt # Python dependencies
└── test/
├── conftest.py # Pytest configuration
├── test_{scenario_name}_scenario.py # Integration tests
└── test_requirements.txt # Test dependencies
CRITICAL: Always read scenarios/features/{servicefeature}/SPECIFICATION.md first to understand:
From the specification, identify:
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
{AWS Service} Feature Scenario
This scenario demonstrates how to use AWS {Service} to {scenario description}.
The scenario includes the following steps:
1. Prepare the Application:
- {Setup step 1 from specification}
- {Setup step 2 from specification}
- Deploy the CloudFormation template for resource creation.
- Store the outputs of the stack into variables for use in the scenario.
2. {Phase 2 Name}:
- {Phase 2 description from specification}
3. {Phase 3 Name}:
- {Phase 3 description from specification}
4. Clean up:
- Prompt the user for y/n answer if they want to destroy the stack and clean up all resources.
- Delete resources created during the workflow.
- Destroy the CloudFormation stack and wait until the stack has been removed.
"""
import time
import uuid
import sys
from typing import Tuple, Optional, Dict, Any, List
import logging
import boto3
from botocore.exceptions import ClientError
from cloudformation_helper import CloudFormationHelper
from {service}_wrapper import {Service}Wrapper
# Add relative path to include demo_tools
sys.path.append("../../../..")
import demo_tools.question as q
logger = logging.getLogger(__name__)
# snippet-start:[python.example_code.{service}.{Service}Scenario]
class {Service}Scenario:
"""Manages the {AWS Service Feature} scenario."""
DASHES = "-" * 80
STACK_NAME = "default-{service}-scenario-stack"
STACK_RESOURCE_PATH = "../../../../../../scenarios/features/{service_feature}/resources/cfn_template.yaml"
def __init__(self, {service}_wrapper: {Service}Wrapper, cfn_helper: CloudFormationHelper) -> None:
"""
Initialize the {AWS Service} scenario.
Args:
{service}_wrapper: {Service}Wrapper instance for AWS service operations
cfn_helper: CloudFormationHelper instance for CloudFormation operations
"""
self.{service}_wrapper = {service}_wrapper
self.cfn_helper = cfn_helper
self._role_arn: Optional[str] = None
self._target_arn: Optional[str] = None
def wait_for_input(self) -> None:
"""Wait for user input to continue."""
q.ask("\nPress Enter to continue...")
print()
def setup_resources(self) -> bool:
"""
Set up initial resources for the scenario.
Returns:
bool: True if setup was successful, False otherwise
"""
print("\nSetting up required resources...")
try:
# Prompt the user for required input (e.g., email, parameters)
print("\nThis example creates resources in a CloudFormation stack.")
user_input = self._prompt_user_for_input()
# Prompt the user for a name for the CloudFormation stack
stack_name = self._prompt_user_for_stack_name()
# Deploy the CloudFormation stack
deploy_success = self._deploy_cloudformation_stack(stack_name, user_input)
if deploy_success:
print("✓ Application preparation complete.")
return True
else:
print("✗ Application preparation failed.")
return False
except Exception as e:
logger.error(f"Error during setup: {e}")
print(f"Error during setup: {e}")
return False
def _deploy_cloudformation_stack(self, stack_name: str, parameter: str) -> bool:
"""
Deploy the CloudFormation stack with the necessary resources.
Args:
stack_name: The name of the CloudFormation stack
parameter: Parameter value for the stack
Returns:
bool: True if the stack was deployed successfully
"""
print(f"\nDeploying CloudFormation stack: {stack_name}")
try:
# Check if template file exists
import os
if not os.path.exists(self.STACK_RESOURCE_PATH):
logger.error(f"CloudFormation template not found: {self.STACK_RESOURCE_PATH}")
return False
# Deploy the stack
success = self.cfn_helper.deploy_cloudformation_stack(
stack_name,
self.STACK_RESOURCE_PATH,
parameters={"ParameterName": parameter} if parameter else None
)
if success:
print(f"✓ CloudFormation stack creation completed: {stack_name}")
# Retrieve the output values
return self._get_stack_outputs(stack_name)
else:
logger.error(f"Failed to create CloudFormation stack: {stack_name}")
return False
except Exception as e:
logger.error(f"Error deploying CloudFormation stack: {e}")
return False
def _get_stack_outputs(self, stack_name: str) -> bool:
"""
Retrieve the output values from the CloudFormation stack.
Args:
stack_name: The name of the CloudFormation stack
Returns:
bool: True if outputs were retrieved successfully
"""
try:
outputs = self.cfn_helper.get_stack_outputs(stack_name)
if outputs:
self._role_arn = outputs.get('RoleARN')
self._target_arn = outputs.get('TargetARN')
if self._role_arn:
print(f"Stack output RoleARN: {self._role_arn}")
if self._target_arn:
print(f"Stack output TargetARN: {self._target_arn}")
return True
else:
logger.error("No stack outputs found")
return False
except Exception as e:
logger.error(f"Failed to retrieve CloudFormation stack outputs: {e}")
return False
def run_scenario(self) -> None:
"""Run the {AWS Service Feature} scenario."""
print(self.DASHES)
print("Welcome to the {AWS Service Feature} Scenario.")
print(self.DASHES)
print("""
This scenario demonstrates how to use {AWS Service} to {feature description}.
The scenario will guide you through:
1. Setting up the necessary AWS resources
2. {Demonstration phase description}
3. {Examination phase description}
4. Cleaning up resources
""")
try:
# Setup Phase
print(self.DASHES)
setup_success = self.setup_resources()
print(self.DASHES)
if setup_success:
# Demonstration Phase
print(self.DASHES)
self._demonstration_phase()
print(self.DASHES)
# Examination Phase
print(self.DASHES)
self._examination_phase()
print(self.DASHES)
# Cleanup Phase
print(self.DASHES)
self._cleanup_phase()
print(self.DASHES)
except Exception as e:
logger.error(f"Scenario failed: {e}")
print(f"There was a problem with the scenario: {e}")
print("\nInitiating cleanup...")
try:
self._cleanup_phase()
except Exception as cleanup_error:
logger.error(f"Error during cleanup: {cleanup_error}")
print("✓ {AWS Service} feature scenario completed.")
print(self.DASHES)
def _demonstration_phase(self) -> None:
"""
Demonstration phase: Implement based on specification.
This phase demonstrates the core service operations as outlined
in the specification's demonstration section.
"""
print("Phase 2: {Demonstration Phase Name}")
print("{Phase description from specification}")
try:
# Implement demonstration operations from specification
# Example: Create resources, perform operations
result = self.{service}_wrapper.perform_operation(self._role_arn, self._target_arn)
if result:
print("✓ Demonstration operations completed successfully")
else:
print("✗ Some demonstration operations encountered issues")
self.wait_for_input()
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
logger.error(f"AWS service error during demonstration: {error_code} - {e}")
print(f"AWS service error: {e}")
except Exception as e:
logger.error(f"Error during demonstration phase: {e}")
print(f"Error during demonstration: {e}")
def _examination_phase(self) -> None:
"""
Examination phase: Implement based on specification.
This phase examines the results and demonstrates data analysis
as outlined in the specification's examination section.
"""
print("Phase 3: {Examination Phase Name}")
print("{Phase description from specification}")
try:
# Implement examination operations from specification
# Example: List resources, analyze data, show results
data = self.{service}_wrapper.list_resources()
if data:
print(f"Found {len(data)} items:")
for i, item in enumerate(data[:5]): # Show first 5 items
print(f" {i+1}. {item}")
if len(data) > 5:
show_more = q.ask(f"Show all {len(data)} items? (y/n): ", q.is_yesno)
if show_more:
for i, item in enumerate(data[5:], 6):
print(f" {i}. {item}")
else:
print("No data found.")
self.wait_for_input()
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
logger.error(f"AWS service error during examination: {error_code} - {e}")
print(f"AWS service error: {e}")
except Exception as e:
logger.error(f"Error during examination phase: {e}")
print(f"Error during examination: {e}")
def _cleanup_phase(self) -> None:
"""
Clean up the resources created during the scenario.
Prompts user for confirmation before deleting resources.
"""
print("Cleanup Phase")
# Prompt the user to confirm cleanup
cleanup = q.ask(
"Do you want to delete all resources created by this scenario? (y/n): ", q.is_yesno
)
if cleanup:
try:
# Delete scenario-specific resources first
print("Deleting scenario resources...")
cleanup_success = self.{service}_wrapper.cleanup_resources()
if cleanup_success:
print("✓ Scenario resources deleted successfully")
# Destroy the CloudFormation stack
print("Deleting CloudFormation stack...")
stack_delete_success = self.cfn_helper.destroy_cloudformation_stack(self.STACK_NAME)
if stack_delete_success:
print("✓ CloudFormation stack deleted successfully")
else:
print("✗ Failed to delete CloudFormation stack")
except Exception as e:
logger.error(f"Error during cleanup: {e}")
print(f"Error during cleanup: {e}")
else:
print("Resources will be retained. You can delete them manually from the AWS Console.")
print("Cleanup complete.")
def _prompt_user_for_stack_name(self) -> str:
"""
Prompt the user for a valid CloudFormation stack name.
Returns:
str: Valid stack name
"""
while True:
stack_name = q.ask("Enter a name for the AWS CloudFormation Stack: ")
if not stack_name:
print("Stack name cannot be empty.")
continue
# Basic validation for CloudFormation stack naming rules
import re
if not re.match(r'^[a-zA-Z][-a-zA-Z0-9]*$', stack_name):
print("Invalid stack name. Use letters, numbers, and hyphens only. Must start with a letter.")
continue
if len(stack_name) > 128:
print("Stack name must be 128 characters or fewer.")
continue
return stack_name
def _prompt_user_for_input(self) -> str:
"""
Prompt the user for required input parameters.
Returns:
str: User input value
"""
# Customize based on scenario requirements
user_input = q.ask("Enter required parameter value: ")
return user_input
# snippet-end:[python.example_code.{service}.{Service}Scenario]
def main() -> None:
"""
Main function to run the {AWS Service} feature scenario.
This example uses the default settings specified in your shared credentials
and config files.
"""
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
try:
# Initialize AWS clients
{service}_client = boto3.client('{service}')
cloudformation_client = boto3.client('cloudformation')
# Initialize wrappers
{service}_wrapper = {Service}Wrapper({service}_client)
cfn_helper = CloudFormationHelper(cloudformation_client)
# Run the scenario
scenario = {Service}Scenario({service}_wrapper, cfn_helper)
scenario.run_scenario()
except Exception as e:
logger.error(f"Failed to run scenario: {e}")
print(f"Failed to run the scenario: {e}")
if __name__ == "__main__":
main()
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
CloudFormation helper for deploying and managing stacks in scenarios.
"""
import json
import time
from typing import Dict, Optional, Any
import logging
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
class CloudFormationHelper:
"""Helper class for CloudFormation operations."""
def __init__(self, cloudformation_client: Any) -> None:
"""
Initialize the CloudFormation helper.
Args:
cloudformation_client: Boto3 CloudFormation client
"""
self.cf_client = cloudformation_client
def deploy_cloudformation_stack(
self,
stack_name: str,
template_path: str,
parameters: Optional[Dict[str, str]] = None
) -> bool:
"""
Deploy a CloudFormation stack.
Args:
stack_name: Name of the stack to create
template_path: Path to the CloudFormation template file
parameters: Optional parameters for the stack
Returns:
bool: True if deployment was successful
"""
try:
# Read template file
with open(template_path, 'r', encoding='utf-8') as template_file:
template_body = template_file.read()
# Prepare create stack request
create_request = {
'StackName': stack_name,
'TemplateBody': template_body,
'Capabilities': ['CAPABILITY_NAMED_IAM']
}
# Add parameters if provided
if parameters:
create_request['Parameters'] = [
{'ParameterKey': key, 'ParameterValue': value}
for key, value in parameters.items()
]
# Create the stack
response = self.cf_client.create_stack(**create_request)
if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f"CloudFormation stack creation started: {stack_name}")
# Wait for stack creation to complete
return self._wait_for_stack_completion(stack_name)
else:
logger.error(f"Failed to create CloudFormation stack: {stack_name}")
return False
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
if error_code == 'AlreadyExistsException':
print(f"CloudFormation stack '{stack_name}' already exists.")
return True
else:
logger.error(f"Error creating CloudFormation stack: {e}")
return False
except Exception as e:
logger.error(f"Error reading template file or creating stack: {e}")
return False
def _wait_for_stack_completion(self, stack_name: str) -> bool:
"""
Wait for CloudFormation stack to complete creation.
Args:
stack_name: Name of the stack to monitor
Returns:
bool: True if stack creation was successful
"""
max_attempts = 60
attempt = 0
delay = 30 # seconds
print("Waiting for CloudFormation stack creation to complete...")
while attempt < max_attempts:
try:
response = self.cf_client.describe_stacks(StackName=stack_name)
if response['Stacks']:
stack_status = response['Stacks'][0]['StackStatus']
print(f"Stack status: {stack_status}")
if stack_status == 'CREATE_COMPLETE':
print("✓ CloudFormation stack creation complete.")
return True
elif stack_status in ['CREATE_FAILED', 'ROLLBACK_COMPLETE']:
print("✗ CloudFormation stack creation failed.")
return False
time.sleep(delay)
attempt += 1
except ClientError as e:
logger.error(f"Error checking stack status: {e}")
return False
logger.error("Timed out waiting for CloudFormation stack creation to complete.")
return False
def get_stack_outputs(self, stack_name: str) -> Optional[Dict[str, str]]:
"""
Get outputs from a CloudFormation stack.
Args:
stack_name: Name of the stack
Returns:
Dict containing stack outputs, or None if not found
"""
try:
response = self.cf_client.describe_stacks(StackName=stack_name)
if response['Stacks']:
stack = response['Stacks'][0]
outputs = {}
for output in stack.get('Outputs', []):
outputs[output['OutputKey']] = output['OutputValue']
return outputs
else:
logger.error(f"No stack found: {stack_name}")
return None
except ClientError as e:
logger.error(f"Error getting stack outputs: {e}")
return None
def destroy_cloudformation_stack(self, stack_name: str) -> bool:
"""
Delete a CloudFormation stack.
Args:
stack_name: Name of the stack to delete
Returns:
bool: True if deletion was successful
"""
try:
self.cf_client.delete_stack(StackName=stack_name)
print(f"CloudFormation stack '{stack_name}' deletion started...")
return self._wait_for_stack_deletion(stack_name)
except ClientError as e:
logger.error(f"Error deleting CloudFormation stack: {e}")
return False
def _wait_for_stack_deletion(self, stack_name: str) -> bool:
"""
Wait for CloudFormation stack deletion to complete.
Args:
stack_name: Name of the stack to monitor
Returns:
bool: True if deletion was successful
"""
max_attempts = 60
attempt = 0
delay = 30 # seconds
print("Waiting for CloudFormation stack deletion to complete...")
while attempt < max_attempts:
try:
response = self.cf_client.describe_stacks(StackName=stack_name)
if response['Stacks']:
stack_status = response['Stacks'][0]['StackStatus']
print(f"Stack status: {stack_status}")
if stack_status == 'DELETE_COMPLETE':
print("✓ CloudFormation stack deletion complete.")
return True
elif stack_status == 'DELETE_FAILED':
print("✗ CloudFormation stack deletion failed.")
return False
time.sleep(delay)
attempt += 1
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
if error_code == 'ValidationError':
# Stack no longer exists
print("✓ CloudFormation stack has been deleted.")
return True
else:
logger.error(f"Error checking stack deletion status: {e}")
return False
logger.error("Timed out waiting for CloudFormation stack deletion to complete.")
return False
CRITICAL: Wrapper classes MUST use the correct snippet tag structure to ensure proper metadata integration and test compatibility.
For Wrapper Class Declaration:
# snippet-start:[python.example_code.{service}.{Service}Wrapper.decl]
class {Service}Wrapper:
"""Wrapper class for managing {Service} operations."""
# Class implementation...
# snippet-end:[python.example_code.{service}.{Service}Wrapper.decl]
For Individual Methods:
# snippet-start:[python.example_code.{service}.MethodName]
def method_name(self, parameter: str) -> bool:
"""Method implementation."""
# Method code...
# snippet-end:[python.example_code.{service}.MethodName]
MANDATORY: When wrapper classes are used in feature scenarios (e.g., topics_and_queues), the metadata files MUST reference the .decl tag pattern:
Correct Metadata Reference:
python/cross_service/example_scenario:
excerpts:
- snippet_tags:
- python.example_code.{service}.{Service}Wrapper.decl # ✓ CORRECT
- python.example_code.{service}.MethodName
Incorrect Metadata Reference:
python/cross_service/example_scenario:
excerpts:
- snippet_tags:
- python.example_code.{service}.{Service}Wrapper # ✗ INCORRECT
- python.example_code.{service}.MethodName
.decl pattern ensures stubbed tests can properly mock wrapper class declarationsIn Wrapper File ({service}_wrapper.py):
# snippet-start:[python.example_code.{servicename}.{Service}Wrapper.decl]
class {Service}Wrapper:
"""Wrapper class for managing {AWS Service} operations."""
def __init__(self, {service}_client: Any) -> None:
"""Initialize the {Service}Wrapper."""
self.{service}_client = {service}_client
# snippet-end:[python.example_code.{servicename}.{Service}Wrapper.decl]
# snippet-start:[python.example_code.{service}.CreateResource]
def create_resource(self, name: str) -> str:
"""Create a resource."""
# Method implementation...
# snippet-end:[python.example_code.{service}.CreateResource]
In Metadata File (.doc_gen/metadata/{service}_metadata.yaml):
For Individual Service Operations:
{service}_CreateResource:
languages:
Python:
versions:
- sdk_version: 3
github: python/cross_service/example_scenario
excerpts:
- snippet_tags:
- python.example_code.{service}.{Service}Wrapper.decl
- python.example_code.{service}.CreateResource
For Scenario Sections (CRITICAL - Must Include Both Scenario and Wrapper Classes):
{service}_Feature_Scenario:
languages:
Python:
versions:
- sdk_version: 3
github: python/cross_service/example_scenario
excerpts:
- description: Run an interactive scenario at a command prompt.
snippet_tags:
- python.example_code.cross_service.example_scenario.ExampleScenario
- description: Create classes that wrap service operations.
snippet_tags:
- python.example_code.{service}.{Service}Wrapper.decl
- python.example_code.{other_service}.{OtherService}Wrapper.decl
Why Both Are Required in Scenario Sections:
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Wrapper class for AWS {Service} operations.
"""
import logging
from typing import List, Dict, Any, Optional
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
# snippet-start:[python.example_code.{serviceExample}.{Service}Wrapper.class]
# snippet-start:[python.example_code.{serviceExample}.{Service}Wrapper.decl]
class {Service}Wrapper:
"""Wrapper class for managing {AWS Service} operations."""
def __init__(self, {service}_client: Any) -> None:
"""
Initialize the {Service}Wrapper.
Args:
{service}_client: A Boto3 {AWS Service} client. This client provides low-level
access to AWS {Service} services.
"""
self.{service}_client = {service}_client
@classmethod
def from_client(cls) -> '{Service}Wrapper':
"""
Create a {Service}Wrapper instance using a default boto3 client.
Returns:
{Service}Wrapper: An instance of this class.
"""
{service}_client = boto3.client('{service}')
return cls({service}_client)
# snippet-end:[python.example_code.{serviceExample}.{Service}Wrapper.decl]
# snippet-start:[python.example_code.{service}.operation_name]
def operation_name(self, parameter: str) -> bool:
"""
Description of what this operation does.
Args:
parameter: Description of parameter
Returns:
bool: True if successful, False otherwise
Raises:
ClientError: If the operation fails
"""
try:
response = self.{service}_client.operation_name(Parameter=parameter)
print(f"✓ Operation completed successfully")
logger.info(f"Operation result: {response}")
return True
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
# Handle specific errors based on specification
if error_code == 'ResourceNotFoundException':
logger.error(f"Resource not found: {e}")
print(f"Resource not found: {parameter}")
return False
elif error_code == 'InvalidParameterException':
logger.error(f"Invalid parameter: {e}")
print(f"Invalid parameter provided: {parameter}")
return False
else:
logger.error(f"Error in operation: {e}")
print(f"An error occurred: {e}")
raise
# snippet-end:[python.example_code.{service}.operation_name]
def list_resources(self) -> List[Dict[str, Any]]:
"""
List resources from the service.
Returns:
List of resource dictionaries
Raises:
ClientError: If the operation fails
"""
try:
response = self.{service}_client.list_resources()
resources = response.get('Resources', [])
logger.info(f"Found {len(resources)} resources")
return resources
except ClientError as e:
logger.error(f"Error listing resources: {e}")
print(f"Error listing resources: {e}")
return []
def cleanup_resources(self) -> bool:
"""
Clean up resources created during the scenario.
Returns:
bool: True if cleanup was successful
Raises:
ClientError: If cleanup fails
"""
try:
# Implement cleanup logic based on scenario requirements
print("Cleaning up scenario resources...")
# Example: Delete created resources
# response = self.{service}_client.delete_resource(ResourceId=resource_id)
print("✓ Resource cleanup completed")
return True
except ClientError as e:
logger.error(f"Error during cleanup: {e}")
print(f"Error during cleanup: {e}")
return False
# snippet-end:[python.example_code.{serviceExample}.{Service}Wrapper.class]
bool for success/failure operationsThe specification includes an "Errors" section with specific error codes and handling:
# Example error handling based on specification
try:
response = self.{service}_wrapper.create_resource()
return response
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
# Handle as specified: Resource already exists
if error_code == 'ConflictException':
logger.error(f"Resource conflict: {e}")
print("Resource already exists. Please choose a different name.")
return False
# Handle as specified: Resource not found
elif error_code == 'ResourceNotFoundException':
logger.error(f"Resource not found: {e}")
print("Required resource not found. Please check your configuration.")
return True # May return True if deletion was the goal
else:
logger.error(f"Unexpected error: {e}")
print(f"An unexpected error occurred: {e}")
raise
boto3>=1.26.137
botocore>=1.29.137
pytest>=7.0.0
pytest-mock>=3.10.0
moto>=4.0.0
MANDATORY: All feature scenarios MUST include both stubbed unit tests and live integration tests:
test_tools for fast, reliable unit testing@pytest.mark.integ decorator to test against real AWS servicesCRITICAL: Before implementing tests, verify that all wrapper methods have corresponding stubber methods available in python/test_tools/{service}_stubber.py.
Common service stubbers available in python/test_tools/:
sns_stubber.py - Amazon SNS operationssqs_stubber.py - Amazon SQS operationss3_stubber.py - Amazon S3 operationsdynamodb_stubber.py - Amazon DynamoDB operationslambda_stubber.py - AWS Lambda operationsiam_stubber.py - AWS IAM operationscloudformation_stubber.py - AWS CloudFormation operations{service}_stubber.py exists in python/test_tools/SNS Wrapper → SNS Stubber Mapping:
# SNS Wrapper Methods → Available Stubber Methods
create_topic() → stub_create_topic()
subscribe_queue_to_topic() → stub_subscribe()
publish_message() → stub_publish()
unsubscribe() → stub_unsubscribe()
delete_topic() → stub_delete_topic()
list_topics() → stub_list_topics()
SQS Wrapper → SQS Stubber Mapping:
# SQS Wrapper Methods → Available Stubber Methods
create_queue() → stub_create_queue()
get_queue_arn() → stub_get_queue_attributes()
set_queue_policy_for_topic() → stub_set_queue_attributes()
receive_messages() → stub_receive_messages()
delete_messages() → stub_delete_message_batch()
delete_queue() → stub_delete_queue()
list_queues() → stub_list_queues()
send_message() → stub_send_message()
If a wrapper method doesn't have a corresponding stubber method:
_stub_bifurcator directlyCRITICAL: Always verify that stubber method parameters match the actual wrapper implementation calls. Mismatched parameters will cause "Unexpected API Call" errors.
Problem: Error getting response stub for operation CreateTopic: Unexpected API Call: A call was made but no additional calls expected.
Root Cause: The wrapper method passes different parameters than what the stubber expects.
Wrapper Implementation Analysis:
# From sns_wrapper.py create_topic method:
attributes = {}
if is_fifo:
attributes['FifoTopic'] = 'true'
if content_based_deduplication:
attributes['ContentBasedDeduplication'] = 'true'
response = self.sns_client.create_topic(
Name=topic_name,
Attributes=attributes # ALWAYS passed, even if empty for standard topics
)
Stubber Method Signature:
# From sns_stubber.py:
def stub_create_topic(self, topic_name, topic_arn, topic_attributes=None, error_code=None):
expected_params = {"Name": topic_name}
if topic_attributes is not None:
expected_params["Attributes"] = topic_attributes
Correct Test Stub Usage:
# For standard (non-FIFO) topic - MUST include empty attributes
runner.add(
mock_mgr.sns_stubber.stub_create_topic,
"test-topic", # topic_name
"arn:aws:sns:us-east-1:123456789012:test-topic", # topic_arn
{}, # topic_attributes (empty dict for standard)
)
# For FIFO topic - include FIFO attributes
runner.add(
mock_mgr.sns_stubber.stub_create_topic,
"test-topic.fifo", # topic_name
"arn:aws:sns:us-east-1:123456789012:test-topic.fifo", # topic_arn
{"FifoTopic": "true"}, # topic_attributes (FIFO settings)
)
Step 1: Enable detailed logging to see actual vs expected parameters:
import logging
logging.basicConfig(level=logging.DEBUG)
Step 2: Compare wrapper client call with stubber expected_params:
expected_params dictionary constructionStep 3: Common parameter patterns:
# SNS Operations
create_topic() → Always passes Attributes (even if empty dict)
subscribe() → May include Attributes for filter policies
publish() → May include MessageAttributes, MessageGroupId, etc.
# SQS Operations
create_queue() → Always passes Attributes (even if empty dict)
receive_message() → Always passes MessageAttributeNames, WaitTimeSeconds
send_message() → May include MessageAttributes, DelaySeconds, etc.
Integration tests should use the established stubbing pattern from test_tools and verify no errors are logged:
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Integration tests for {Service} scenario.
"""
import pytest
from unittest.mock import patch
import logging
import boto3
import sys
import os
# Add parent directory to path to import scenario modules
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Add test_tools to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "test_tools"))
from {scenario_name}_scenario import {Service}Scenario
from {service}_wrapper import {Service}Wrapper
class MockManager:
"""Mock manager for the {Service} scenario tests."""
def __init__(self, {service}_client, {service}_stubber, stub_runner):
"""{Service} test setup manager."""
self.{service}_client = {service}_client
self.{service}_stubber = {service}_stubber
self.stub_runner = stub_runner
self.{service}_wrapper = {Service}Wrapper({service}_client)
self.scenario = {Service}Scenario(self.{service}_wrapper)
@pytest.fixture
def mock_mgr(make_stubber, stub_runner):
"""Create a mock manager with AWS service stubbers."""
{service}_client = boto3.client('{service}')
{service}_stubber = make_stubber({service}_client)
return MockManager({service}_client, {service}_stubber, stub_runner)
class TestScenarioIntegration:
"""Integration tests for the {Service} scenario."""
@patch('demo_tools.question.ask')
def test_scenario_integration_no_errors_logged(self, mock_ask, mock_mgr, caplog):
"""
Verify the scenario runs without logging any errors.
Args:
mock_ask: Mock for user input
mock_mgr: Mock manager with stubbed AWS clients
caplog: Pytest log capture fixture
"""
# Arrange user inputs
mock_ask.side_effect = [
"test-resource-name", # Resource name input
"test-parameter", # Parameter input
True, # Cleanup confirmation
]
# Set up stubs for AWS operations
with mock_mgr.stub_runner(None, None) as runner:
# Add stubs for each AWS operation the scenario will perform
# Example for SNS operations:
runner.add(
mock_mgr.sns_stubber.stub_create_topic,
"test-topic",
"arn:aws:sns:us-east-1:123456789012:test-topic",
)
runner.add(
mock_mgr.sns_stubber.stub_subscribe,
"arn:aws:sns:us-east-1:123456789012:test-topic",
"sqs",
"arn:aws:sqs:us-east-1:123456789012:test-queue",
"arn:aws:sns:us-east-1:123456789012:test-topic:subscription-id",
)
runner.add(
mock_mgr.sns_stubber.stub_publish,
"Test message",
"message-id-123",
topic_arn="arn:aws:sns:us-east-1:123456789012:test-topic",
)
runner.add(
mock_mgr.sns_stubber.stub_delete_topic,
"arn:aws:sns:us-east-1:123456789012:test-topic",
)
# Example for SQS operations:
runner.add(
mock_mgr.sqs_stubber.stub_create_queue,
"test-queue",
{},
"https://sqs.us-east-1.amazonaws.com/123456789012/test-queue",
)
runner.add(
mock_mgr.sqs_stubber.stub_get_queue_attributes,
"https://sqs.us-east-1.amazonaws.com/123456789012/test-queue",
"arn:aws:sqs:us-east-1:123456789012:test-queue",
)
runner.add(
mock_mgr.sqs_stubber.stub_receive_messages,
"https://sqs.us-east-1.amazonaws.com/123456789012/test-queue",
[{"body": "Test message"}],
1,
)
runner.add(
mock_mgr.sqs_stubber.stub_delete_queue,
"https://sqs.us-east-1.amazonaws.com/123456789012/test-queue",
)
# Act - Run the scenario
with caplog.at_level(logging.ERROR):
mock_mgr.scenario.run_scenario()
# Assert no errors logged
error_logs = [record for record in caplog.records if record.levelno >= logging.ERROR]
assert len(error_logs) == 0, f"Expected no error logs, but found: {error_logs}"
@patch('demo_tools.question.ask')
def test_scenario_handles_aws_errors_gracefully(self, mock_ask, mock_mgr):
"""Test that the scenario handles AWS errors gracefully."""
# Arrange
mock_ask.side_effect = [
"test-resource-name", # Resource name
]
# Set up stub to simulate AWS error
with mock_mgr.stub_runner("TestException", 0) as runner:
runner.add(
mock_mgr.{service}_stubber.stub_create_resource,
"test-resource-name",
"arn:aws:{service}:us-east-1:123456789012:resource/test-resource",
)
# Act & Assert - Should handle error gracefully
mock_mgr.scenario.run_scenario()
# Verify error was logged appropriately (scenario should continue or cleanup)
### Live Integration Test Pattern
**MANDATORY**: Always include a live integration test that uses real AWS services:
```python
@pytest.mark.integ
def test_run_scenario_integ(input_mocker, capsys):
"""Test the scenario with an integration test using live AWS services."""
# Mock user inputs for automated testing
answers = [
"test-resource-name-integ", # Resource name
"test-parameter", # Parameter input
True, # Cleanup confirmation
]
input_mocker.mock_answers(answers)
# Create real AWS clients (no stubs)
{service}_client = boto3.client('{service}')
# Initialize wrappers and scenario with real clients
{service}_wrapper = {Service}Wrapper({service}_client)
scenario = {Service}Scenario({service}_wrapper)
# Run the scenario with live AWS services
scenario.run_scenario()
# Verify the scenario completed successfully
captured = capsys.readouterr()
assert "scenario completed successfully" in captured.out
Key Features of Live Integration Tests:
@pytest.mark.integ decorator - Allows selective test executioninput_mocker to simulate user interactionsRunning Integration Tests:
# Run only unit tests (stubbed)
pytest test/test_scenario.py
# Run only integration tests (live AWS)
pytest test/test_scenario.py -m integ
# Run all tests
pytest test/test_scenario.py -m "not integ" && pytest test/test_scenario.py -m integ