diff --git a/.doc_gen/metadata/neptune_metadata.yaml b/.doc_gen/metadata/neptune_metadata.yaml index 6a616a3e6cc..821bd4a020c 100644 --- a/.doc_gen/metadata/neptune_metadata.yaml +++ b/.doc_gen/metadata/neptune_metadata.yaml @@ -5,6 +5,14 @@ neptune_Hello: synopsis: get started using &neptune;. category: Hello languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.hello.main Java: versions: - sdk_version: 2 @@ -18,6 +26,14 @@ neptune_Hello: neptune: {DescribeDBClustersPaginator} neptune_ExecuteQuery: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.graph.execute.main Java: versions: - sdk_version: 2 @@ -31,6 +47,14 @@ neptune_ExecuteQuery: neptune: {ExecuteQuery} neptune_CreateGraph: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.graph.create.main Java: versions: - sdk_version: 2 @@ -44,6 +68,14 @@ neptune_CreateGraph: neptune: {CreateGraph} neptune_ExecuteOpenCypherExplainQuery: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.data.query.opencypher.main Java: versions: - sdk_version: 2 @@ -57,6 +89,14 @@ neptune_ExecuteOpenCypherExplainQuery: neptune: {ExecuteOpenCypherExplainQuery} neptune_ExecuteGremlinProfileQuery: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.data.query.gremlin.main Java: versions: - sdk_version: 2 @@ -70,6 +110,14 @@ neptune_ExecuteGremlinProfileQuery: neptune: {ExecuteGremlinProfileQuery} neptune_ExecuteGremlinQuery: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.data.query.gremlin.profile.main Java: versions: - sdk_version: 2 @@ -83,6 +131,14 @@ neptune_ExecuteGremlinQuery: neptune: {ExecuteGremlinQuery} neptune_DeleteDBSubnetGroup: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.delete.subnet.group.main Java: versions: - sdk_version: 2 @@ -96,6 +152,14 @@ neptune_DeleteDBSubnetGroup: neptune: {DeleteDBSubnetGroup} neptune_DeleteDBCluster: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.delete.cluster.main Java: versions: - sdk_version: 2 @@ -109,6 +173,14 @@ neptune_DeleteDBCluster: neptune: {DeleteDBCluster} neptune_DeleteDBInstance: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.delete.instance.main Java: versions: - sdk_version: 2 @@ -122,6 +194,14 @@ neptune_DeleteDBInstance: neptune: {DeleteDBInstance} neptune_StartDBCluster: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.start.cluster.main Java: versions: - sdk_version: 2 @@ -135,6 +215,14 @@ neptune_StartDBCluster: neptune: {StartDBCluster} neptune_StopDBCluster: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.stop.cluster.main Java: versions: - sdk_version: 2 @@ -148,6 +236,14 @@ neptune_StopDBCluster: neptune: {StopDBCluster} neptune_DescribeDBClusters: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.describe.cluster.main Java: versions: - sdk_version: 2 @@ -161,6 +257,14 @@ neptune_DescribeDBClusters: neptune: {DescribeDBClusters} neptune_DescribeDBInstances: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.describe.dbinstance.main Java: versions: - sdk_version: 2 @@ -174,6 +278,14 @@ neptune_DescribeDBInstances: neptune: {DescribeDBInstances} neptune_CreateDBInstance: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.create.dbinstance.main Java: versions: - sdk_version: 2 @@ -187,6 +299,14 @@ neptune_CreateDBInstance: neptune: {CreateDBInstance} neptune_CreateDBCluster: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.create.cluster.main Java: versions: - sdk_version: 2 @@ -200,6 +320,14 @@ neptune_CreateDBCluster: neptune: {CreateDBCluster} neptune_CreateDBSubnetGroup: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.create.subnet.main Java: versions: - sdk_version: 2 @@ -223,6 +351,14 @@ neptune_Scenario: - Delete the &neptune; Assets. category: Basics languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/neptune + excerpts: + - description: + snippet_tags: + - neptune.python.scenario.main Java: versions: - sdk_version: 2 diff --git a/python/example_code/neptune/README.md b/python/example_code/neptune/README.md new file mode 100644 index 00000000000..df881f4d6bc --- /dev/null +++ b/python/example_code/neptune/README.md @@ -0,0 +1,142 @@ +# Neptune code examples for the SDK for Python + +## Overview + +Shows how to use the AWS SDK for Python (Boto3) to work with Amazon Neptune. + + + + +_Neptune is a serverless graph database designed for superior scalability and availability._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](http://aws.amazon.com/pricing/) and [Free Tier](http://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](http://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](http://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `python` folder. + +Install the packages required by these examples by running the following in a virtual environment: + +``` +python -m pip install -r requirements.txt +``` + + + + +### Get started + +- [Hello Neptune](HelloNeptune.py#L4) (`DescribeDBClustersPaginator`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](NeptuneScenario.py) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreateDBCluster](NeptuneScenario.py#L288) +- [CreateDBInstance](NeptuneScenario.py#L269) +- [CreateDBSubnetGroup](NeptuneScenario.py#L335) +- [CreateGraph](analytics/CreateNeptuneGraphExample.py#L7) +- [DeleteDBCluster](NeptuneScenario.py#L14) +- [DeleteDBInstance](NeptuneScenario.py#L77) +- [DeleteDBSubnetGroup](NeptuneScenario.py#L95) +- [DescribeDBClusters](NeptuneScenario.py#L203) +- [DescribeDBInstances](NeptuneScenario.py#L242) +- [ExecuteGremlinProfileQuery](database/NeptuneGremlinQueryExample.py#L22) +- [ExecuteGremlinQuery](database/NeptuneGremlinExplainAndProfileExample.py#L8) +- [ExecuteOpenCypherExplainQuery](database/OpenCypherExplainExample.py#L22) +- [ExecuteQuery](analytics/NeptuneAnalyticsQueryExample.py#L7) +- [StartDBCluster](NeptuneScenario.py#L161) +- [StopDBCluster](NeptuneScenario.py#L183) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello Neptune + +This example shows you how to get started using Neptune. + +``` +python HelloNeptune.py +``` + +#### Learn the basics + +This example shows you how to do the following: + +- Create an Amazon Neptune Subnet Group. +- Create an Neptune Cluster. +- Create an Neptune Instance. +- Check the status of the Neptune Instance. +- Show Neptune cluster details. +- Stop the Neptune cluster. +- Start the Neptune cluster. +- Delete the Neptune Assets. + + + + +Start the example by running the following at a command prompt: + +``` +python NeptuneScenario.py +``` + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `python` folder. + + + + + + +## Additional resources + +- [Neptune User Guide](http://docs.aws.amazon.com/neptune/latest/userguide/intro.html) +- [Neptune API Reference](http://docs.aws.amazon.com/neptune/latest/apiref/Welcome.html) +- [SDK for Python Neptune reference](http://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/neptune/analytics/create_neptune_graph_example.py b/python/example_code/neptune/analytics/create_neptune_graph_example.py new file mode 100644 index 00000000000..dcb4847a748 --- /dev/null +++ b/python/example_code/neptune/analytics/create_neptune_graph_example.py @@ -0,0 +1,56 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +from botocore.exceptions import ClientError, BotoCoreError +from botocore.config import Config +# snippet-start:[neptune.python.graph.create.main] +""" +Running this example. + +---------------------------------------------------------------------------------- +VPC Networking Requirement: +---------------------------------------------------------------------------------- +Amazon Neptune must be accessed from **within the same VPC** as the Neptune cluster. +It does not expose a public endpoint, so this code must be executed from: + + - An **AWS Lambda function** configured to run inside the same VPC + - An **EC2 instance** or **ECS task** running in the same VPC + - A connected environment such as a **VPN**, **AWS Direct Connect**, or a **peered VPC** + +""" + +GRAPH_NAME = "sample-analytics-graph" + +def main(): + config = Config(retries={"total_max_attempts": 1, "mode": "standard"}, read_timeout=None) + client = boto3.client("neptune-graph", config=config) + execute_create_graph(client, GRAPH_NAME) + +def execute_create_graph(client, graph_name): + try: + print("Creating Neptune graph...") + response = client.create_graph( + graphName=graph_name, + provisionedMemory = 16 + ) + + created_graph_name = response.get("name") + graph_arn = response.get("arn") + graph_endpoint = response.get("endpoint") + + print("Graph created successfully!") + print(f"Graph Name: {created_graph_name}") + print(f"Graph ARN: {graph_arn}") + print(f"Graph Endpoint: {graph_endpoint}") + + except ClientError as e: + print(f"Failed to create graph: {e.response['Error']['Message']}") + except BotoCoreError as e: + print(f"Failed to create graph: {str(e)}") + except Exception as e: + print(f"Unexpected error: {str(e)}") + +if __name__ == "__main__": + main() +# snippet-end:[neptune.python.graph.create.main] \ No newline at end of file diff --git a/python/example_code/neptune/analytics/neptune_analytics_query_example.py b/python/example_code/neptune/analytics/neptune_analytics_query_example.py new file mode 100644 index 00000000000..9ae8aaf2286 --- /dev/null +++ b/python/example_code/neptune/analytics/neptune_analytics_query_example.py @@ -0,0 +1,104 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +from botocore.exceptions import ClientError, BotoCoreError +from botocore.config import Config + +# snippet-start:[neptune.python.graph.execute.main] +""" +Running this example. + +---------------------------------------------------------------------------------- +VPC Networking Requirement: +---------------------------------------------------------------------------------- +Amazon Neptune must be accessed from **within the same VPC** as the Neptune cluster. +It does not expose a public endpoint, so this code must be executed from: + + - An **AWS Lambda function** configured to run inside the same VPC + - An **EC2 instance** or **ECS task** running in the same VPC + - A connected environment such as a **VPN**, **AWS Direct Connect**, or a **peered VPC** +""" + +GRAPH_ID = "" + +def main(): + config = Config(retries={"total_max_attempts": 1, "mode": "standard"}, read_timeout=None) + client = boto3.client("neptune-graph", config=config) + + try: + print("\n--- Running OpenCypher query without parameters ---") + run_open_cypher_query(client, GRAPH_ID) + + print("\n--- Running OpenCypher query with parameters ---") + run_open_cypher_query_with_params(client, GRAPH_ID) + + print("\n--- Running OpenCypher explain query ---") + run_open_cypher_explain_query(client, GRAPH_ID) + + except Exception as e: + print(f"Unexpected error in main: {e}") + +def run_open_cypher_query(client, graph_id): + """ + Run an OpenCypher query without parameters. + """ + try: + resp = client.execute_query( + graphIdentifier=graph_id, + queryString="MATCH (n {code: 'ANC'}) RETURN n", + language='OPEN_CYPHER' + ) + print(resp['payload'].read().decode('UTF-8')) + + except client.exceptions.InternalServerException as e: + print(f"InternalServerException: {e.response['Error']['Message']}") + except ClientError as e: + print(f"ClientError: {e.response['Error']['Message']}") + except Exception as e: # <--- ADD THIS BLOCK + print(f"Unexpected error: {e}") + +def run_open_cypher_query_with_params(client, graph_id): + """ + Run an OpenCypher query with parameters. + """ + try: + parameters = {'code': 'ANC'} + resp = client.execute_query( + graphIdentifier=graph_id, + queryString="MATCH (n {code: $code}) RETURN n", + language='OPEN_CYPHER', + parameters=parameters + ) + print(resp['payload'].read().decode('UTF-8')) + + except client.exceptions.InternalServerException as e: + print(f"InternalServerException: {e.response['Error']['Message']}") + except ClientError as e: + print(f"ClientError: {e.response['Error']['Message']}") + except Exception as e: # <--- ADD THIS BLOCK + print(f"Unexpected error: {e}") + +def run_open_cypher_explain_query(client, graph_id): + """ + Run an OpenCypher explain query (explainMode = "debug"). + """ + try: + resp = client.execute_query( + graphIdentifier=graph_id, + queryString="MATCH (n {code: 'ANC'}) RETURN n", + language='OPEN_CYPHER', + explainMode='DETAILS' + ) + print(resp['payload'].read().decode('UTF-8')) + + except ClientError as e: + print(f"Neptune error: {e.response['Error']['Message']}") + except BotoCoreError as e: + print(f"Unexpected Boto3 error: {str(e)}") + except Exception as e: # <-- Add this generic catch + print(f"Unexpected error: {str(e)}") + +if __name__ == "__main__": + main() +# snippet-end:[neptune.python.graph.execute.main] \ No newline at end of file diff --git a/python/example_code/neptune/database/neptune_execute_gremlin_explain_query.py b/python/example_code/neptune/database/neptune_execute_gremlin_explain_query.py new file mode 100644 index 00000000000..1587b643083 --- /dev/null +++ b/python/example_code/neptune/database/neptune_execute_gremlin_explain_query.py @@ -0,0 +1,64 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import boto3 +from botocore.config import Config +from botocore.exceptions import BotoCoreError, ClientError + +""" +Running this example. + +---------------------------------------------------------------------------------- +VPC Networking Requirement: +---------------------------------------------------------------------------------- +Amazon Neptune must be accessed from **within the same VPC** as the Neptune cluster. +It does not expose a public endpoint, so this code must be executed from: + + - An **AWS Lambda function** configured to run inside the same VPC + - An **EC2 instance** or **ECS task** running in the same VPC + - A connected environment such as a **VPN**, **AWS Direct Connect**, or a **peered VPC** + +""" +# snippet-start:[neptune.python.data.query.gremlin.main] +# Replace this with your actual Neptune endpoint +NEPTUNE_ENDPOINT = "http://[Specify Endpoint]:8182" + +def main(): + """ + Entry point of the program. Initializes the Neptune client and executes the Gremlin query. + """ + config = Config(connect_timeout=10, read_timeout=30, retries={'max_attempts': 3}) + + neptune_client = boto3.client( + "neptunedata", + endpoint_url=NEPTUNE_ENDPOINT, + config=config + ) + + execute_gremlin_query(neptune_client) + + +def execute_gremlin_query(neptune_client): + """ + Executes a Gremlin query against an Amazon Neptune database. + """ + try: + print("Querying Neptune...") + + response = neptune_client.execute_gremlin_explain_query( + gremlinQuery="g.V().has('code', 'ANC')" + ) + + print("Full Response:") + print(response['output'].read().decode('UTF-8')) + + except ClientError as e: + print(f"Error calling Neptune: {e.response['Error']['Message']}") + except BotoCoreError as e: + print(f"BotoCore error: {str(e)}") + except Exception as e: + print(f"Unexpected error: {str(e)}") + + +if __name__ == "__main__": + main() +# snippet-end:[neptune.python.data.query.gremlin.main] \ No newline at end of file diff --git a/python/example_code/neptune/database/neptune_execute_gremlin_profile_query.py b/python/example_code/neptune/database/neptune_execute_gremlin_profile_query.py new file mode 100644 index 00000000000..ade11a5fab2 --- /dev/null +++ b/python/example_code/neptune/database/neptune_execute_gremlin_profile_query.py @@ -0,0 +1,70 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +from botocore.config import Config +from botocore.exceptions import BotoCoreError, ClientError + +# snippet-start:[neptune.python.data.query.gremlin.profile.main] +""" +Running this example. + +---------------------------------------------------------------------------------- +VPC Networking Requirement: +---------------------------------------------------------------------------------- +Amazon Neptune must be accessed from **within the same VPC** as the Neptune cluster. +It does not expose a public endpoint, so this code must be executed from: + + - An **AWS Lambda function** configured to run inside the same VPC + - An **EC2 instance** or **ECS task** running in the same VPC + - A connected environment such as a **VPN**, **AWS Direct Connect**, or a **peered VPC** + +""" + +# Replace with your actual Neptune endpoint +NEPTUNE_ENDPOINT = "http://[Specify-Your-Endpoint]:8182" + +def main(): + """ + Entry point of the program. Initializes the Neptune client and runs both EXPLAIN and PROFILE queries. + """ + config = Config(connect_timeout=10, read_timeout=30, retries={'max_attempts': 3}) + + neptune_client = boto3.client( + "neptunedata", + endpoint_url=NEPTUNE_ENDPOINT, + config=config + ) + + try: + run_profile_query(neptune_client) + except ClientError as e: + print(f"Neptune error: {e.response['Error']['Message']}") + except BotoCoreError as e: + print(f"BotoCore error: {str(e)}") + except Exception as e: + print(f"Unexpected error: {str(e)}") + +def run_profile_query(neptune_client): + """ + Runs a PROFILE query on the Neptune graph database. + """ + print("Running Gremlin PROFILE query...") + + try: + response = neptune_client.execute_gremlin_profile_query( + gremlinQuery="g.V().has('code', 'ANC')" + ) + print("Profile Query Result:") + output = response.get("output") + if output: + print(output.read().decode('utf-8')) + else: + print("No explain output returned.") + except Exception as e: + print(f"Failed to execute PROFILE query: {str(e)}") + + +if __name__ == "__main__": + main() +# snippet-end:[neptune.python.data.query.gremlin.profile.main] \ No newline at end of file diff --git a/python/example_code/neptune/database/neptune_execute_gremlin_query.py b/python/example_code/neptune/database/neptune_execute_gremlin_query.py new file mode 100644 index 00000000000..76a889cbfd7 --- /dev/null +++ b/python/example_code/neptune/database/neptune_execute_gremlin_query.py @@ -0,0 +1,68 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start: [neptune.python.data.query.gremlin.profile.main] +import boto3 +from botocore.config import Config +from botocore.exceptions import BotoCoreError, ClientError + +""" +Running this example. + +---------------------------------------------------------------------------------- +VPC Networking Requirement: +---------------------------------------------------------------------------------- +Amazon Neptune must be accessed from **within the same VPC** as the Neptune cluster. +It does not expose a public endpoint, so this code must be executed from: + + - An **AWS Lambda function** configured to run inside the same VPC + - An **EC2 instance** or **ECS task** running in the same VPC + - A connected environment such as a **VPN**, **AWS Direct Connect**, or a **peered VPC** + +""" +# Customize this with your Neptune endpoint +NEPTUNE_ENDPOINT = "http://:8182" + +def execute_gremlin_query(client): + """ + Executes a Gremlin query using the provided Neptune Data client. + """ + print("Executing Gremlin query...") + + try: + response = client.execute_gremlin_query( + gremlinQuery="g.V().has('code', 'ANC')" + ) + + print("Response is:") + print(response['result']) + + except ClientError as e: + print(f"Neptune error: {e.response['Error']['Message']}") + except BotoCoreError as e: + print(f"Unexpected Boto3 error: {str(e)}") + except Exception as e: + print(f"Unexpected error: {str(e)}") + +def main(): + """ + Main entry point: creates the Neptune client and runs the profile query. + """ + + # * To prevent unneccesary retries please set the total_max_attempts to 1 + # * To prevent a read timeout on the client when a query runs longer than 60 seconds set the read_timeout to None + config = Config(retries={"total_max_attempts": 1, "mode": "standard"}, read_timeout=None) + + neptune_client = boto3.client( + "neptunedata", + endpoint_url=NEPTUNE_ENDPOINT, + config=config + ) + + execute_gremlin_query(neptune_client) + + +if __name__ == "__main__": + main() + +# snippet-end: [neptune.python.data.query.gremlin.profile.main] \ No newline at end of file diff --git a/python/example_code/neptune/database/neptune_execute_open_cypher_query.py b/python/example_code/neptune/database/neptune_execute_open_cypher_query.py new file mode 100644 index 00000000000..0ccfddff078 --- /dev/null +++ b/python/example_code/neptune/database/neptune_execute_open_cypher_query.py @@ -0,0 +1,107 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import json +from botocore.config import Config +from botocore.exceptions import ClientError, BotoCoreError + +""" +Running this example. + +---------------------------------------------------------------------------------- +VPC Networking Requirement: +---------------------------------------------------------------------------------- +Amazon Neptune must be accessed from **within the same VPC** as the Neptune cluster. +It does not expose a public endpoint, so this code must be executed from: + + - An **AWS Lambda function** configured to run inside the same VPC + - An **EC2 instance** or **ECS task** running in the same VPC + - A connected environment such as a **VPN**, **AWS Direct Connect**, or a **peered VPC** + +""" +# snippet-start:[neptune.python.data.query.opencypher.main] + +# Replace with your actual Neptune endpoint URL +NEPTUNE_ENDPOINT = "http://:8182" + +def main(): + """ + Entry point: Create Neptune client and execute different OpenCypher queries. + """ + config = Config(connect_timeout=10, read_timeout=30, retries={'max_attempts': 3}) + + neptune_client = boto3.client( + "neptunedata", + endpoint_url=NEPTUNE_ENDPOINT, + config=config + ) + + execute_open_cypher_query_without_params(neptune_client) + execute_open_cypher_query_with_params(neptune_client) + execute_open_cypher_explain_query(neptune_client) + +def execute_open_cypher_query_without_params(client): + """ + Executes a simple OpenCypher query without parameters. + """ + try: + print("\nRunning OpenCypher query without parameters...") + resp = client.execute_open_cypher_query( + openCypherQuery="MATCH (n {code: 'ANC'}) RETURN n" + ) + print("Results:") + print(resp['results']) + + except Exception as e: + print(f"Error in simple OpenCypher query: {str(e)}") + + +def execute_open_cypher_query_with_params(client): + """ + Executes an OpenCypher query using parameters. + """ + try: + print("\nRunning OpenCypher query with parameters...") + parameters = {'code': 'ANC'} + resp = client.execute_open_cypher_query( + openCypherQuery="MATCH (n {code: $code}) RETURN n", + parameters=json.dumps(parameters) + ) + print("Results:") + print(resp['results']) + + except Exception as e: + print(f"Error in parameterized OpenCypher query: {str(e)}") + +def execute_open_cypher_explain_query(client): + """ + Runs an OpenCypher EXPLAIN query in debug mode. + """ + try: + print("\nRunning OpenCypher EXPLAIN query (debug mode)...") + resp = client.execute_open_cypher_explain_query( + openCypherQuery="MATCH (n {code: 'ANC'}) RETURN n", + explainMode="details" + ) + results = resp.get('results') + if results is None: + print("No explain results returned.") + else: + try: + print("Explain Results:") + print(results.read().decode('UTF-8')) + except Exception as e: + print(f"Error in OpenCypher EXPLAIN query: {str(e)}") + + except ClientError as e: + print(f"Neptune error: {e.response['Error']['Message']}") + except BotoCoreError as e: + print(f"BotoCore error: {str(e)}") + except Exception as e: + print(f"Unexpected error: {str(e)}") + + +if __name__ == "__main__": + main() +# snippet-end:[neptune.python.data.query.opencypher.main]# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/python/example_code/neptune/hello_neptune.py b/python/example_code/neptune/hello_neptune.py new file mode 100644 index 00000000000..82d332efdc1 --- /dev/null +++ b/python/example_code/neptune/hello_neptune.py @@ -0,0 +1,53 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[neptune.python.hello.main] +import boto3 +from botocore.exceptions import ClientError + + +def describe_db_clusters(neptune_client): + """ + Describes the Amazon Neptune DB clusters using a paginator to handle multiple pages. + Raises ClientError with 'ResourceNotFoundException' if no clusters are found. + """ + paginator = neptune_client.get_paginator("describe_db_clusters") + clusters_found = False + + for page in paginator.paginate(): + for cluster in page.get("DBClusters", []): + clusters_found = True + print(f"Cluster Identifier: {cluster['DBClusterIdentifier']}") + print(f"Status: {cluster['Status']}") + + if not clusters_found: + raise ClientError( + { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "No Neptune DB clusters found." + } + }, + operation_name="DescribeDBClusters" + ) + +def main(): + """ + Main entry point: creates the Neptune client and calls the describe operation. + """ + neptune_client = boto3.client("neptune") + try: + describe_db_clusters(neptune_client) + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "ResourceNotFoundException": + print(f"Resource not found: {e.response['Error']['Message']}") + else: + print(f"Unexpected ClientError: {e.response['Error']['Message']}") + except Exception as e: + print(f"Unexpected error: {str(e)}") + +if __name__ == "__main__": + main() + +# snippet-end:[neptune.python.hello.main] \ No newline at end of file diff --git a/python/example_code/neptune/neptune_scenario.py b/python/example_code/neptune/neptune_scenario.py new file mode 100644 index 00000000000..78343ef4464 --- /dev/null +++ b/python/example_code/neptune/neptune_scenario.py @@ -0,0 +1,704 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + + +# snippet-start:[neptune.python.scenario.main] +import boto3 +import time +from botocore.exceptions import ClientError + +# Constants used in this scenario +POLL_INTERVAL_SECONDS = 10 +TIMEOUT_SECONDS = 1200 # 20 minutes + +# snippet-start:[neptune.python.delete.cluster.main] +def delete_db_cluster(neptune_client, cluster_id: str): + """ + Deletes a Neptune DB cluster and throws exceptions to the caller. + + Args: + neptune_client (boto3.client): The Neptune client object. + cluster_id (str): The ID of the Neptune DB cluster to be deleted. + + Raises: + ClientError: If the delete operation fails. + """ + request = { + 'DBClusterIdentifier': cluster_id, + 'SkipFinalSnapshot': True + } + + try: + print(f"Deleting DB Cluster: {cluster_id}") + neptune_client.delete_db_cluster(**request) + + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "DBClusterNotFoundFault": + print(f"Cluster '{cluster_id}' not found or already deleted.") + elif code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"Couldn't delete DB cluster. {code}: {message}") + raise +# snippet-end:[neptune.python.delete.cluster.main] + +def format_elapsed_time(seconds: int) -> str: + mins, secs = divmod(seconds, 60) + hours, mins = divmod(mins, 60) + return f"{hours:02}:{mins:02}:{secs:02}" + + +# snippet-start:[neptune.python.delete.instance.main] +def delete_db_instance(neptune_client, instance_id: str): + """ + Deletes a Neptune DB instance and waits for its deletion to complete. + Raises exception to be handled by calling code. + """ + print(f"Initiating deletion of DB Instance: {instance_id}") + try: + neptune_client.delete_db_instance( + DBInstanceIdentifier=instance_id, + SkipFinalSnapshot=True + ) + + print(f"Waiting for DB Instance '{instance_id}' to be deleted...") + waiter = neptune_client.get_waiter('db_instance_deleted') + waiter.wait( + DBInstanceIdentifier=instance_id, + WaiterConfig={ + 'Delay': 30, + 'MaxAttempts': 40 + } + ) + + print(f"DB Instance '{instance_id}' successfully deleted.") + + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "DBInstanceNotFoundFault": + print(f"Instance '{instance_id}' not found or already deleted.") + elif code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"Couldn't delete DB instance. {code}: {message}") + raise +# snippet-end:[neptune.python.delete.instance.main] + +# snippet-start:[neptune.python.delete.subnet.group.main] +def delete_db_subnet_group(neptune_client, subnet_group_name): + """ + Deletes a Neptune DB subnet group synchronously using Boto3. + + Args: + neptune_client (boto3.client): The Neptune client. + subnet_group_name (str): The name of the DB subnet group to delete. + + Raises: + ClientError: If the delete operation fails. + """ + delete_group_request = { + 'DBSubnetGroupName': subnet_group_name + } + + try: + neptune_client.delete_db_subnet_group(**delete_group_request) + print(f"ļø Deleting Subnet Group: {subnet_group_name}") + + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "DBSubnetGroupNotFoundFault": + print(f"Subnet group '{subnet_group_name}' not found or already deleted.") + elif code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"Couldn't delete subnet group. {code}: {message}") + raise +# snippet-end:[neptune.python.delete.subnet.group.main] + +def wait_for_cluster_status( + neptune_client, + cluster_id: str, + desired_status: str, + timeout_seconds: int = TIMEOUT_SECONDS, + poll_interval_seconds: int = POLL_INTERVAL_SECONDS +): + """ + Waits for a Neptune DB cluster to reach a desired status. + + Args: + neptune_client (boto3.client): The Amazon Neptune client. + cluster_id (str): The identifier of the Neptune DB cluster. + desired_status (str): The target status (e.g., "available", "stopped"). + timeout_seconds (int): Max time to wait in seconds (default: 1200). + poll_interval_seconds (int): Polling interval in seconds (default: 10). + + Raises: + RuntimeError: If the desired status is not reached before timeout. + """ + print(f"Waiting for cluster '{cluster_id}' to reach status '{desired_status}'...") + start_time = time.time() + + while True: + # Prepare request object + describe_cluster_request = { + 'DBClusterIdentifier': cluster_id + } + + # Call the Neptune API + response = neptune_client.describe_db_clusters(**describe_cluster_request) + clusters = response.get('DBClusters', []) + current_status = clusters[0].get('Status') if clusters else None + elapsed_seconds = int(time.time() - start_time) + + status_str = current_status if current_status else "Unknown" + print( + f"\r Elapsed: {format_elapsed_time(elapsed_seconds):<20} Cluster status: {status_str:<20}", + end="", flush=True + ) + + if current_status and current_status.lower() == desired_status.lower(): + print( + f"\nNeptune cluster reached desired status '{desired_status}' after {format_elapsed_time(elapsed_seconds)}." + ) + return + + if elapsed_seconds > timeout_seconds: + raise RuntimeError(f"Timeout waiting for Neptune cluster to reach status: {desired_status}") + + time.sleep(poll_interval_seconds) + + +# snippet-start:[neptune.python.start.cluster.main] +def start_db_cluster(neptune_client, cluster_identifier: str): + """ + Starts an Amazon Neptune DB cluster and waits until it reaches 'available'. + + Args: + neptune_client (boto3.client): The Neptune client. + cluster_identifier (str): The DB cluster identifier. + + Raises: + ClientError: Propagates AWS API issues like resource not found. + RuntimeError: If cluster doesn't reach 'available' within timeout. + """ + try: + # Initial wait in case the cluster was just stopped + time.sleep(30) + neptune_client.start_db_cluster(DBClusterIdentifier=cluster_identifier) + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"Couldn't start DB cluster. Here's why: {code}: {message}") + raise + + start_time = time.time() + paginator = neptune_client.get_paginator('describe_db_clusters') + + while True: + try: + pages = paginator.paginate(DBClusterIdentifier=cluster_identifier) + clusters = [] + for page in pages: + clusters.extend(page.get('DBClusters', [])) + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "DBClusterNotFound": + print(f"Cluster '{cluster_identifier}' not found while polling. It may have been deleted.") + else: + print(f"Couldn't describe DB cluster. Here's why: {code}: {message}") + raise + + status = clusters[0].get('Status') if clusters else None + elapsed = time.time() - start_time + + print(f"\rElapsed: {int(elapsed)}s – Cluster status: {status}", end="", flush=True) + + if status and status.lower() == 'available': + print(f"\nšŸŽ‰ Cluster '{cluster_identifier}' is available.") + return + + if elapsed > TIMEOUT_SECONDS: + raise RuntimeError(f"Timeout waiting for cluster '{cluster_identifier}' to become available.") + + time.sleep(POLL_INTERVAL_SECONDS) + +# snippet-end:[neptune.python.start.cluster.main] + +# snippet-start:[neptune.python.stop.cluster.main] +def stop_db_cluster(neptune_client, cluster_identifier: str): + """ + Stops an Amazon Neptune DB cluster and waits until it's fully stopped. + + Args: + neptune_client (boto3.client): The Neptune client. + cluster_identifier (str): The DB cluster identifier. + + Raises: + ClientError: For AWS API errors (e.g., resource not found). + RuntimeError: If the cluster doesn't stop within the timeout. + """ + try: + neptune_client.stop_db_cluster(DBClusterIdentifier=cluster_identifier) + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"Couldn't stop DB cluster. Here's why: {code}: {message}") + raise + + start_time = time.time() + paginator = neptune_client.get_paginator('describe_db_clusters') + + while True: + try: + pages = paginator.paginate(DBClusterIdentifier=cluster_identifier) + clusters = [] + for page in pages: + clusters.extend(page.get('DBClusters', [])) + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "DBClusterNotFound": + print(f"Cluster '{cluster_identifier}' not found while polling. It may have been deleted.") + else: + print(f"Couldn't describe DB cluster. Here's why: {code}: {message}") + raise + + status = clusters[0].get('Status') if clusters else None + elapsed = time.time() - start_time + + print(f"\rElapsed: {int(elapsed)}s – Cluster status: {status}", end="", flush=True) + + if status and status.lower() == 'stopped': + print(f"\nCluster '{cluster_identifier}' is now stopped.") + return + + if elapsed > TIMEOUT_SECONDS: + raise RuntimeError(f"Timeout waiting for cluster '{cluster_identifier}' to stop.") + + time.sleep(POLL_INTERVAL_SECONDS) + + +# snippet-end:[neptune.python.stop.cluster.main] + +# snippet-start:[neptune.python.describe.cluster.main] +def describe_db_clusters(neptune_client, cluster_id: str): + """ + Describes details of a Neptune DB cluster, paginating if needed. + + Args: + neptune_client (boto3.client): The Neptune client. + cluster_id (str): The ID of the cluster to describe. + + Raises: + ClientError: If there's an AWS API error (e.g., cluster not found). + """ + paginator = neptune_client.get_paginator('describe_db_clusters') + + try: + pages = paginator.paginate(DBClusterIdentifier=cluster_id) + + found = False + for page in pages: + for cluster in page.get('DBClusters', []): + found = True + print(f"Cluster Identifier: {cluster.get('DBClusterIdentifier')}") + print(f"Status: {cluster.get('Status')}") + print(f"Engine: {cluster.get('Engine')}") + print(f"Engine Version: {cluster.get('EngineVersion')}") + print(f"Endpoint: {cluster.get('Endpoint')}") + print(f"Reader Endpoint: {cluster.get('ReaderEndpoint')}") + print(f"Availability Zones: {cluster.get('AvailabilityZones')}") + print(f"Subnet Group: {cluster.get('DBSubnetGroup')}") + print("VPC Security Groups:") + for vpc_group in cluster.get('VpcSecurityGroups', []): + print(f" - {vpc_group.get('VpcSecurityGroupId')}") + print(f"Storage Encrypted: {cluster.get('StorageEncrypted')}") + print(f"IAM Auth Enabled: {cluster.get('IAMDatabaseAuthenticationEnabled')}") + print(f"Backup Retention Period: {cluster.get('BackupRetentionPeriod')} days") + print(f"Preferred Backup Window: {cluster.get('PreferredBackupWindow')}") + print(f"Preferred Maintenance Window: {cluster.get('PreferredMaintenanceWindow')}") + print("------") + + if not found: + # Treat empty response as cluster not found + raise ClientError( + {"Error": {"Code": "DBClusterNotFound", "Message": f"No cluster found with ID '{cluster_id}'"}}, + "DescribeDBClusters" + ) + + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + elif code == "DBClusterNotFound": + print(f"Cluster '{cluster_id}' not found. Please verify the cluster ID.") + else: + print(f"Couldn't describe DB cluster. Here's why: {code}: {message}") + raise +# snippet-end:[neptune.python.describe.cluster.main] + +# snippet-start:[neptune.python.describe.dbinstance.main] +def check_instance_status(neptune_client, instance_id: str, desired_status: str): + """ + Polls the status of a Neptune DB instance until it reaches desired_status. + Uses pagination via describe_db_instances — even for a single instance. + + Raises: + ClientError: If describe_db_instances fails (e.g., instance not found). + RuntimeError: If timeout expires before reaching desired status. + """ + paginator = neptune_client.get_paginator('describe_db_instances') + start_time = time.time() + + while True: + try: + pages = paginator.paginate(DBInstanceIdentifier=instance_id) + instances = [] + for page in pages: + instances.extend(page.get('DBInstances', [])) + + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "DBInstanceNotFound": + print(f"Instance '{instance_id}' not found. Please verify the instance ID.") + else: + print(f"Failed to describe DB instance. {code}: {message}") + raise + + current_status = instances[0].get('DBInstanceStatus') if instances else None + elapsed = int(time.time() - start_time) + + print(f"\rElapsed: {format_elapsed_time(elapsed)} Status: {current_status}", end="", flush=True) + + if current_status and current_status.lower() == desired_status.lower(): + print(f"\nInstance '{instance_id}' reached '{desired_status}' in {format_elapsed_time(elapsed)}.") + return + + if elapsed > TIMEOUT_SECONDS: + raise RuntimeError(f"Timeout waiting for '{instance_id}' to reach '{desired_status}'") + + time.sleep(POLL_INTERVAL_SECONDS) + +# snippet-end:[neptune.python.describe.dbinstance.main] + +# snippet-start:[neptune.python.create.dbinstance.main] +def create_db_instance(neptune_client, db_instance_id: str, db_cluster_id: str) -> str: + try: + request = { + 'DBInstanceIdentifier': db_instance_id, + 'DBInstanceClass': 'db.r5.large', + 'Engine': 'neptune', + 'DBClusterIdentifier': db_cluster_id + } + + print(f"Creating Neptune DB Instance: {db_instance_id}") + response = neptune_client.create_db_instance(**request) + + instance = response.get('DBInstance') + if not instance or 'DBInstanceIdentifier' not in instance: + raise RuntimeError("Instance creation succeeded but no ID returned.") + + print(f"Waiting for DB Instance '{db_instance_id}' to become available...") + waiter = neptune_client.get_waiter('db_instance_available') + waiter.wait( + DBInstanceIdentifier=db_instance_id, + WaiterConfig={'Delay': 30, 'MaxAttempts': 40} + ) + + print(f"DB Instance '{db_instance_id}' is now available.") + return instance['DBInstanceIdentifier'] + + except ClientError as err: + code = err.response["Error"]["Code"] + message = err.response["Error"]["Message"] + + if code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"Couldn't create DB instance. Here's why: {code}: {message}") + raise + + except Exception as e: + print(f"Unexpected error creating DB instance '{db_instance_id}': {e}") + raise RuntimeError(f"Unexpected error creating DB instance '{db_instance_id}': {e}") from e + +# snippet-end:[neptune.python.create.dbinstance.main] + +# snippet-start:[neptune.python.create.cluster.main] +def create_db_cluster(neptune_client, db_name: str) -> str: + """ + Creates a Neptune DB cluster and returns its identifier. + + Args: + neptune_client (boto3.client): The Neptune client object. + db_name (str): The desired cluster identifier. + + Returns: + str: The DB cluster identifier. + + Raises: + RuntimeError: For any failure or AWS error, with a user-friendly message. + """ + request = { + 'DBClusterIdentifier': db_name, + 'Engine': 'neptune', + 'DeletionProtection': False, + 'BackupRetentionPeriod': 1 + } + + try: + response = neptune_client.create_db_cluster(**request) + cluster = response.get('DBCluster') or {} + + cluster_id = cluster.get('DBClusterIdentifier') + if not cluster_id: + raise RuntimeError("Cluster created but no ID returned.") + + print(f"DB Cluster created: {cluster_id}") + return cluster_id + + except ClientError as e: + code = e.response["Error"]["Code"] + message = e.response["Error"]["Message"] + + if code in ("ServiceQuotaExceededException", "DBClusterQuotaExceededFault"): + raise RuntimeError("You have exceeded the quota for Neptune DB clusters.") from e + else: + raise RuntimeError(f"AWS error [{code}]: {message}") from e + + except Exception as e: + raise RuntimeError(f"Unexpected error creating DB cluster '{db_name}': {e}") from e +# snippet-end:[neptune.python.create.cluster.main] + +def get_subnet_ids(vpc_id: str) -> list[str]: + ec2_client = boto3.client('ec2') + + describe_subnets_request = { + 'Filters': [{'Name': 'vpc-id', 'Values': [vpc_id]}] + } + + response = ec2_client.describe_subnets(**describe_subnets_request) + subnets = response.get('Subnets', []) + subnet_ids = [subnet['SubnetId'] for subnet in subnets if 'SubnetId' in subnet] + return subnet_ids + + +def get_default_vpc_id() -> str: + ec2_client = boto3.client('ec2') + describe_vpcs_request = { + 'Filters': [{'Name': 'isDefault', 'Values': ['true']}] + } + + response = ec2_client.describe_vpcs(**describe_vpcs_request) + vpcs = response.get('Vpcs', []) + if not vpcs: + raise RuntimeError("No default VPC found in this region.") + + default_vpc_id = vpcs[0]['VpcId'] + print(f"Default VPC ID: {default_vpc_id}") + return default_vpc_id + + +# snippet-start:[neptune.python.create.subnet.main] +def create_subnet_group(neptune_client, group_name: str): + """ + Creates a Neptune DB subnet group and returns its name and ARN. + + Args: + neptune_client (boto3.client): The Neptune client object. + group_name (str): The desired name of the subnet group. + + Returns: + tuple(str, str): (subnet_group_name, subnet_group_arn) + + Raises: + RuntimeError: For quota errors or other AWS-related failures. + """ + vpc_id = get_default_vpc_id() + subnet_ids = get_subnet_ids(vpc_id) + + request = { + 'DBSubnetGroupName': group_name, + 'DBSubnetGroupDescription': 'My Neptune subnet group', + 'SubnetIds': subnet_ids, + 'Tags': [{'Key': 'Environment', 'Value': 'Dev'}] + } + + try: + response = neptune_client.create_db_subnet_group(**request) + sg = response.get("DBSubnetGroup", {}) + name = sg.get("DBSubnetGroupName") + arn = sg.get("DBSubnetGroupArn") + + if not name or not arn: + raise RuntimeError("Response missing subnet group name or ARN.") + + print(f"Subnet group created: {name}") + print(f"ARN: {arn}") + return name, arn + + except ClientError as e: + code = e.response["Error"]["Code"] + msg = e.response["Error"]["Message"] + + if code == "ServiceQuotaExceededException": + print("Subnet group quota exceeded.") + raise RuntimeError("Subnet group quota exceeded.") from e + else: + print(f"AWS error [{code}]: {msg}") + raise RuntimeError(f"AWS error [{code}]: {msg}") from e + + except Exception as e: + print(f"Unexpected error creating subnet group '{group_name}': {e}") + raise RuntimeError(f"Unexpected error creating subnet group '{group_name}': {e}") from e +# snippet-end:[neptune.python.create.subnet.main] + +def wait_for_input_to_continue(): + input("\nPress to continue...") + print("Continuing with the program...\n") + + +def run_scenario(neptune_client, subnet_group_name: str, db_instance_id: str, cluster_name: str): + print("-" * 88) + print("1. Create a Neptune DB Subnet Group") + wait_for_input_to_continue() + + try: + name, arn = create_subnet_group(neptune_client, subnet_group_name) + print(f"Subnet group successfully created: {name}") + + print("-" * 88) + print("2. Create a Neptune Cluster") + wait_for_input_to_continue() + db_cluster_id = create_db_cluster(neptune_client, cluster_name) + + print("-" * 88) + print("3. Create a Neptune DB Instance") + wait_for_input_to_continue() + create_db_instance(neptune_client, db_instance_id, cluster_name) + + print("-" * 88) + print("4. Check the status of the Neptune DB Instance") + print(""" + Even though you're targeting a single DB instance, + describe_db_instances supports pagination and can return multiple pages. + + Handling paginated responses ensures your method continues to work reliably + even if AWS returns large or paged results. + """) + wait_for_input_to_continue() + check_instance_status(neptune_client, db_instance_id, "available") + + print("-" * 88) + print("5. Show Neptune Cluster details") + wait_for_input_to_continue() + describe_db_clusters(neptune_client, db_cluster_id) + + print("-" * 88) + print("6. Stop the Amazon Neptune cluster") + print(""" + Boto3 doesn't currently offer a + built-in waiter for stop_db_cluster, + This example implements a custom polling + strategy until the cluster is in a stopped state. + """) + wait_for_input_to_continue() + stop_db_cluster(neptune_client, db_cluster_id) + check_instance_status(neptune_client, db_instance_id, "stopped") + + print("-" * 88) + print("7. Start the Amazon Neptune cluster") + print(""" + Boto3 doesn't currently offer a + built-in waiter for start_db_cluster, + This example implements a custom polling + strategy until the cluster is in an available state. + """) + wait_for_input_to_continue() + start_db_cluster(neptune_client, db_cluster_id) + wait_for_cluster_status(neptune_client, db_cluster_id, "available") + check_instance_status(neptune_client, db_instance_id, "available") + + print("All Neptune resources are now available.") + print("-" * 88) + + print("-" * 88) + print("8. Delete the Neptune Assets") + print("Would you like to delete the Neptune Assets? (y/n)") + del_ans = input().strip().lower() + + if del_ans == "y": + print("You selected to delete the Neptune assets.") + + delete_db_instance(neptune_client, db_instance_id) + delete_db_cluster(neptune_client, db_cluster_id) + delete_db_subnet_group(neptune_client, subnet_group_name) + + print("Neptune resources deleted successfully") + + except ClientError as ce: + code = ce.response["Error"]["Code"] + + if code in ("DBInstanceNotFound", "DBInstanceNotFoundFault", "ResourceNotFound"): + print(f"Instance '{db_instance_id}' not found.") + elif code in ("DBClusterNotFound", "DBClusterNotFoundFault", "ResourceNotFoundFault"): + print(f"Cluster '{cluster_name}' not found.") + elif code == "DBSubnetGroupNotFoundFault": + print(f"Subnet group '{subnet_group_name}' not found.") + elif code == "AccessDeniedException": + print("Access denied. Please ensure you have the necessary permissions.") + else: + print(f"AWS error [{code}]: {ce.response['Error']['Message']}") + raise # re-raise unexpected errors + + except RuntimeError as re: + print(f"Runtime error or timeout: {re}") + + +def main(): + neptune_client = boto3.client('neptune') + + # Customize the following names to match your Neptune setup + # (You must change these to unique values for your environment) + subnet_group_name = "neptuneSubnetGroup111" + cluster_name = "neptuneCluster111" + db_instance_id = "neptuneDB111" + + print(""" + Amazon Neptune is a fully managed graph database service by AWS... + Let's get started! + """) + wait_for_input_to_continue() + run_scenario(neptune_client, subnet_group_name, db_instance_id, cluster_name) + + print(""" + Thank you for checking out the Amazon Neptune Service Use demo. + For more AWS code examples, visit: + http://docs.aws.amazon.com/code-library/latest/ug/what-is-code-library.html + """) + +if __name__ == "__main__": + main() +# snippet-end:[neptune.python.scenario.main] \ No newline at end of file diff --git a/python/example_code/neptune/requirements.txt b/python/example_code/neptune/requirements.txt new file mode 100644 index 00000000000..621e276912d --- /dev/null +++ b/python/example_code/neptune/requirements.txt @@ -0,0 +1,2 @@ +boto3>=1.26.79 +pytest>=7.2.1 diff --git a/python/example_code/neptune/tests/analytics_tests/test_create_graph.py b/python/example_code/neptune/tests/analytics_tests/test_create_graph.py new file mode 100644 index 00000000000..c11f442e6c8 --- /dev/null +++ b/python/example_code/neptune/tests/analytics_tests/test_create_graph.py @@ -0,0 +1,53 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from botocore.exceptions import ClientError, BotoCoreError +from example_code.neptune.analytics.create_neptune_graph_example import execute_create_graph +from test_tools.neptune_graph_stubber import NeptuneGraphStubber + +class MockBotoCoreError(BotoCoreError): + def __init__(self, message="BotoCore error occurred"): + super().__init__() + self.message = message + + def __str__(self): + return self.message + +def test_execute_create_graph(capfd): + stubber = NeptuneGraphStubber() + client = stubber.get_client() + stubber.activate() + + stubber.add_create_graph_stub("test-graph") + execute_create_graph(client, "test-graph") + out, _ = capfd.readouterr() + assert "Creating Neptune graph..." in out + assert "Graph created successfully!" in out + assert "Graph Name: test-graph" in out + assert "Graph ARN: arn:aws:neptune-graph:us-east-1:123456789012:graph/test-graph" in out + assert "Graph Endpoint: http://test-graph.cluster-neptune.amazonaws.com" in out + + stubber.deactivate() + + def raise_client_error(**kwargs): + raise ClientError( + {"Error": {"Message": "Client error occurred"}}, "CreateGraph" + ) + client.create_graph = raise_client_error + execute_create_graph(client, "test-graph") + out, _ = capfd.readouterr() + assert "Failed to create graph: Client error occurred" in out + + def raise_boto_core_error(**kwargs): + raise MockBotoCoreError() + client.create_graph = raise_boto_core_error + execute_create_graph(client, "test-graph") + out, _ = capfd.readouterr() + assert "Failed to create graph: BotoCore error occurred" in out + + def raise_generic_error(**kwargs): + raise Exception("Generic failure") + client.create_graph = raise_generic_error + execute_create_graph(client, "test-graph") + out, _ = capfd.readouterr() + assert "Unexpected error: Generic failure" in out diff --git a/python/example_code/neptune/tests/analytics_tests/test_execute_gremlin_profile_query.py b/python/example_code/neptune/tests/analytics_tests/test_execute_gremlin_profile_query.py new file mode 100644 index 00000000000..032c94d99b0 --- /dev/null +++ b/python/example_code/neptune/tests/analytics_tests/test_execute_gremlin_profile_query.py @@ -0,0 +1,75 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import io +from botocore.response import StreamingBody +from example_code.neptune.analytics.neptune_analytics_query_example import run_open_cypher_query +from test_tools.neptune_graph_stubber import NeptuneGraphStubber + +GRAPH_ID = "test-graph-id" + +def test_execute_gremlin_profile_query(capfd): + stubber = NeptuneGraphStubber() + client = stubber.get_client() + + stubber.activate() + payload_bytes = b'{"results": "some data"}' + response_body = StreamingBody(io.BytesIO(payload_bytes), len(payload_bytes)) + + stubber.stubber.add_response( + "execute_query", + {"payload": response_body}, + { + "graphIdentifier": GRAPH_ID, + "queryString": "MATCH (n {code: 'ANC'}) RETURN n", + "language": "OPEN_CYPHER" + } + ) + run_open_cypher_query(client, GRAPH_ID) + out, _ = capfd.readouterr() + assert '{"results": "some data"}' in out + stubber.deactivate() + + stubber.activate() + empty_payload = StreamingBody(io.BytesIO(b''), 0) + + stubber.stubber.add_response( + "execute_query", + {"payload": empty_payload}, + { + "graphIdentifier": GRAPH_ID, + "queryString": "MATCH (n {code: 'ANC'}) RETURN n", + "language": "OPEN_CYPHER" + } + ) + run_open_cypher_query(client, GRAPH_ID) + out, _ = capfd.readouterr() + assert out.strip() == "" + stubber.deactivate() + + stubber.activate() + stubber.stubber.add_client_error( + "execute_query", + service_error_code="ValidationException", + service_message="Client error occurred", + http_status_code=400, + expected_params={ + "graphIdentifier": GRAPH_ID, + "queryString": "MATCH (n {code: 'ANC'}) RETURN n", + "language": "OPEN_CYPHER" + } + ) + run_open_cypher_query(client, GRAPH_ID) + out, _ = capfd.readouterr() + assert "ClientError: Client error occurred" in out + stubber.deactivate() + + stubber.deactivate() + + def raise_generic_error(**kwargs): + raise Exception("Generic failure") + + client.execute_query = raise_generic_error + run_open_cypher_query(client, GRAPH_ID) + out, _ = capfd.readouterr() + assert "Unexpected error: Generic failure" in out diff --git a/python/example_code/neptune/tests/database_tests/execute_gremlin_profile_query.py b/python/example_code/neptune/tests/database_tests/execute_gremlin_profile_query.py new file mode 100644 index 00000000000..726e5ef876a --- /dev/null +++ b/python/example_code/neptune/tests/database_tests/execute_gremlin_profile_query.py @@ -0,0 +1,72 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import types +from botocore.exceptions import ClientError, BotoCoreError +from test_tools.neptune_data_stubber import NeptuneDateStubber +from example_code.neptune.database.neptune_execute_gremlin_profile_query import run_profile_query + +def test_run_profile_query(capfd): + stubber = NeptuneDateStubber() + client = stubber.get_client() + stubber.activate() + + try: + # --- Success case with streaming output --- + profile_response_payload = '{"metrics": {"dur": 500, "steps": 3}}' + stubber.add_execute_gremlin_profile_query_stub( + gremlin_query="g.V().has('code', 'ANC')", + response_payload=profile_response_payload + ) + run_profile_query(client) + out, _ = capfd.readouterr() + assert "Running Gremlin PROFILE query..." in out + assert "Profile Query Result:" in out + assert '"dur": 500' in out or "'dur': 500" in out + + # --- Success case with no output (output=None) --- + stubber.stubber.assert_no_pending_responses() + stubber.add_execute_gremlin_profile_query_stub( + gremlin_query="g.V().has('code', 'ANC')", + response_payload="" # Empty string simulates no output + ) + run_profile_query(client) + out, _ = capfd.readouterr() + # Because output is streaming body, empty string means output.read() returns '', so "No explain output returned." should NOT print + # So, test that something is printed (could be empty) + assert "Profile Query Result:" in out + + # --- ClientError case --- + stubber.stubber.assert_no_pending_responses() + stubber.stubber.add_client_error( + method='execute_gremlin_profile_query', + service_error_code='BadRequest', + service_message='Invalid query', + expected_params={"gremlinQuery": "g.V().has('code', 'ANC')"} + ) + run_profile_query(client) + out, _ = capfd.readouterr() + assert "Failed to execute PROFILE query:" in out or "Neptune error:" in out or "Invalid query" in out + + # --- BotoCoreError case --- + stubber.stubber.assert_no_pending_responses() + + def raise_boto_core_error(*args, **kwargs): + raise BotoCoreError() + + client.execute_gremlin_profile_query = types.MethodType(raise_boto_core_error, client) + run_profile_query(client) + out, _ = capfd.readouterr() + assert "Failed to execute PROFILE query:" in out or "BotoCore error" in out or "Unexpected Boto3 error" in out + + # --- Generic Exception case --- + def raise_generic_exception(*args, **kwargs): + raise Exception("Boom") + + client.execute_gremlin_profile_query = types.MethodType(raise_generic_exception, client) + run_profile_query(client) + out, _ = capfd.readouterr() + assert "Failed to execute PROFILE query: Boom" in out + + finally: + stubber.deactivate() diff --git a/python/example_code/neptune/tests/database_tests/test_execute_gremlin_query.py b/python/example_code/neptune/tests/database_tests/test_execute_gremlin_query.py new file mode 100644 index 00000000000..68519c48c76 --- /dev/null +++ b/python/example_code/neptune/tests/database_tests/test_execute_gremlin_query.py @@ -0,0 +1,66 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import types +from botocore.exceptions import ClientError, BotoCoreError +from test_tools.neptune_data_stubber import NeptuneDateStubber +from example_code.neptune.database.neptune_execute_gremlin_query import execute_gremlin_query + +def test_execute_gremlin_query(capfd): + stubber = NeptuneDateStubber() + client = stubber.get_client() + stubber.activate() + + try: + stubber.add_execute_gremlin_query_stub( + gremlin_query="g.V().has('code', 'ANC')", + response_dict={"result": {"metrics": {"dur": 500, "steps": 3}}} + ) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Executing Gremlin query..." in out + assert "Response is:" in out + assert '"dur": 500' in out or "'dur': 500" in out + + stubber.stubber.assert_no_pending_responses() + stubber.add_execute_gremlin_query_stub( + gremlin_query="g.V().has('code', 'ANC')", + response_dict={"result": None} + ) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Response is:" in out + assert "None" in out + + stubber.stubber.assert_no_pending_responses() + stubber.stubber.add_client_error( + method='execute_gremlin_query', + service_error_code='BadRequest', + service_message='Invalid query', + expected_params={"gremlinQuery": "g.V().has('code', 'ANC')"} + ) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Neptune error: Invalid query" in out + + stubber.stubber.assert_no_pending_responses() + + def raise_boto_core_error(*args, **kwargs): + raise BotoCoreError() # āœ… No arguments + + client.execute_gremlin_query = types.MethodType(raise_boto_core_error, client) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Unexpected Boto3 error" in out + + # --- Generic Exception case --- + def raise_generic_exception(*args, **kwargs): + raise Exception("Boom") + + client.execute_gremlin_query = types.MethodType(raise_generic_exception, client) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Unexpected error: Boom" in out + + finally: + stubber.deactivate() diff --git a/python/example_code/neptune/tests/database_tests/test_neptune_execute_gremlin_explain_query.py b/python/example_code/neptune/tests/database_tests/test_neptune_execute_gremlin_explain_query.py new file mode 100644 index 00000000000..4a41d759df4 --- /dev/null +++ b/python/example_code/neptune/tests/database_tests/test_neptune_execute_gremlin_explain_query.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import types +from botocore.exceptions import ClientError, EndpointConnectionError +from test_tools.neptune_data_stubber import NeptuneDateStubber +from example_code.neptune.database.neptune_execute_gremlin_explain_query import execute_gremlin_query + +def test_execute_gremlin_query(capfd): + stubber = NeptuneDateStubber() + client = stubber.get_client() + stubber.activate() + + try: + response_payload = '{"explain": "details"}' + stubber.add_execute_gremlin_explain_query_stub( + gremlin_query="g.V().has('code', 'ANC')", + response_payload=response_payload + ) + + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Querying Neptune..." in out + assert "Full Response:" in out + assert '{"explain": "details"}' in out + + stubber.stubber.assert_no_pending_responses() + stubber.stubber.add_client_error( + method='execute_gremlin_explain_query', + service_error_code='BadRequest', + service_message='Invalid query', + expected_params={"gremlinQuery": "g.V().has('code', 'ANC')"} + ) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Error calling Neptune: Invalid query" in out + + stubber.stubber.assert_no_pending_responses() + + def raise_endpoint_connection_error(*args, **kwargs): + raise EndpointConnectionError(endpoint_url="http://neptune.amazonaws.com") + + client.execute_gremlin_explain_query = types.MethodType(raise_endpoint_connection_error, client) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "BotoCore error:" in out + + # --- Unexpected Exception case --- + def raise_generic_exception(*args, **kwargs): + raise Exception("Boom") + + client.execute_gremlin_explain_query = types.MethodType(raise_generic_exception, client) + execute_gremlin_query(client) + out, _ = capfd.readouterr() + assert "Unexpected error: Boom" in out + + finally: + stubber.deactivate() diff --git a/python/example_code/neptune/tests/database_tests/test_opencypher_explain_query.py b/python/example_code/neptune/tests/database_tests/test_opencypher_explain_query.py new file mode 100644 index 00000000000..ea539dbfa04 --- /dev/null +++ b/python/example_code/neptune/tests/database_tests/test_opencypher_explain_query.py @@ -0,0 +1,87 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import types +from botocore.exceptions import ClientError, BotoCoreError +from botocore.response import StreamingBody +from test_tools.neptune_data_stubber import NeptuneDateStubber +from example_code.neptune.database.neptune_execute_open_cypher_query import execute_open_cypher_explain_query +import io + +def test_execute_opencypher_explain_query(capfd): + stubber = NeptuneDateStubber() + client = stubber.get_client() + stubber.activate() + + try: + explain_payload = 'mocked byte explain output' + stubber.add_execute_open_cypher_explain_query_stub( + open_cypher_query="MATCH (n {code: 'ANC'}) RETURN n", + explain_mode="details", + results_payload=explain_payload + ) + execute_open_cypher_explain_query(client) + out, _ = capfd.readouterr() + assert "Explain Results:" in out + assert explain_payload in out + + stubber.stubber.assert_no_pending_responses() + + empty_stream = StreamingBody(io.BytesIO(b""), 0) + stubber.stubber.add_response( + "execute_open_cypher_explain_query", + {"results": empty_stream}, + { + "openCypherQuery": "MATCH (n {code: 'ANC'}) RETURN n", + "explainMode": "details" + } + ) + execute_open_cypher_explain_query(client) + out, _ = capfd.readouterr() + assert "Explain Results:" in out + assert out.strip().endswith("Explain Results:") + + stubber.stubber.assert_no_pending_responses() + + finally: + stubber.deactivate() + + def run_client_error_case(): + stubber = NeptuneDateStubber() + client = stubber.get_client() + stubber.activate() + try: + stubber.stubber.add_client_error( + method='execute_open_cypher_explain_query', + service_error_code='BadRequest', + service_message='Invalid OpenCypher query', + expected_params={ + "openCypherQuery": "MATCH (n {code: 'ANC'}) RETURN n", + "explainMode": "details" + } + ) + execute_open_cypher_explain_query(client) + out, _ = capfd.readouterr() + assert "Neptune error: Invalid OpenCypher query" in out + finally: + stubber.deactivate() + + run_client_error_case() + + def raise_boto_core_error(*args, **kwargs): + raise BotoCoreError() + + client.execute_open_cypher_explain_query = types.MethodType(raise_boto_core_error, client) + execute_open_cypher_explain_query(client) + out, _ = capfd.readouterr() + assert "BotoCore error:" in out + + def raise_generic_exception(*args, **kwargs): + raise Exception("Some generic error") + + client.execute_open_cypher_explain_query = types.MethodType(raise_generic_exception, client) + execute_open_cypher_explain_query(client) + out, _ = capfd.readouterr() + assert "Unexpected error: Some generic error" in out + + diff --git a/python/example_code/neptune/tests/test_check_instance_status.py b/python/example_code/neptune/tests/test_check_instance_status.py new file mode 100644 index 00000000000..8b9a5ae5a94 --- /dev/null +++ b/python/example_code/neptune/tests/test_check_instance_status.py @@ -0,0 +1,101 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from botocore.exceptions import ClientError +import boto3 +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import check_instance_status + +def test_check_instance_status_with_neptune_stubber(monkeypatch): + client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(client) + + instance_id = "instance-1" + + stubbed_pages_starting = [{"DBInstances": [{"DBInstanceStatus": "starting"}]}] + stubbed_pages_available = [{"DBInstances": [{"DBInstanceStatus": "available"}]}] + + stubber.stubber.add_response( + "describe_db_instances", + stubbed_pages_starting[0], + expected_params={"DBInstanceIdentifier": instance_id}, + ) + stubber.stubber.add_response( + "describe_db_instances", + stubbed_pages_available[0], + expected_params={"DBInstanceIdentifier": instance_id}, + ) + + times = [0, 1, 2, 3, 4, 5] + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.time.time", + lambda: times.pop(0) if times else 5, + ) + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.time.sleep", lambda s: None + ) + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.format_elapsed_time", lambda x: f"{x}s" + ) + + check_instance_status(stubber.client, instance_id, "available") + + +def test_check_instance_status_timeout(monkeypatch): + client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(client) + + instance_id = "instance-timeout" + + stub_response = {"DBInstances": [{"DBInstanceStatus": "starting"}]} + + for _ in range(10): + stubber.stubber.add_response( + "describe_db_instances", + stub_response, + expected_params={"DBInstanceIdentifier": instance_id}, + ) + + times = list(range(15)) + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.time.time", + lambda: times.pop(0) if times else 15, + ) + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.time.sleep", lambda s: None + ) + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.format_elapsed_time", lambda x: f"{x}s" + ) + + monkeypatch.setattr("example_code.neptune.neptune_scenario.TIMEOUT_SECONDS", 5) + monkeypatch.setattr("example_code.neptune.neptune_scenario.POLL_INTERVAL_SECONDS", 1) + + with pytest.raises(RuntimeError, match=f"Timeout waiting for '{instance_id}'"): + check_instance_status(stubber.client, instance_id, "available") + +def test_check_instance_status_client_error(monkeypatch): + client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(client) + + instance_id = "not-there" + + stubber.stubber.add_client_error( + "describe_db_instances", + service_error_code="DBInstanceNotFound", + service_message="Instance not found", + expected_params={"DBInstanceIdentifier": instance_id}, + ) + + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.time.sleep", lambda s: None + ) + monkeypatch.setattr( + "example_code.neptune.neptune_scenario.format_elapsed_time", lambda x: f"{x}s" + ) + + with pytest.raises(ClientError, match="Instance not found"): + check_instance_status(stubber.client, instance_id, "available") + + diff --git a/python/example_code/neptune/tests/test_create_db_cluster.py b/python/example_code/neptune/tests/test_create_db_cluster.py new file mode 100644 index 00000000000..98c4d94098b --- /dev/null +++ b/python/example_code/neptune/tests/test_create_db_cluster.py @@ -0,0 +1,45 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import boto3 +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import create_db_cluster + +def test_create_db_cluster(): + boto_client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(boto_client) + + stubber.stub_create_db_cluster( + cluster_id="test-cluster" + ) + cluster_id = create_db_cluster(stubber.client, "test-cluster") + assert cluster_id == "test-cluster" + + stubber.stubber.add_response( + "create_db_cluster", + {"DBCluster": {}}, + expected_params={ + "DBClusterIdentifier": "missing-id-cluster", + "Engine": "neptune", + "DeletionProtection": False, + "BackupRetentionPeriod": 1 + } + ) + with pytest.raises(RuntimeError, match="Cluster created but no ID returned"): + create_db_cluster(stubber.client, "missing-id-cluster") + + stubber.stub_create_db_cluster( + cluster_id="denied-cluster", + error_code="AccessDenied" + ) + with pytest.raises(RuntimeError) as exc_info: + create_db_cluster(stubber.client, "denied-cluster") + assert "AWS error [AccessDenied]" in str(exc_info.value) + + def raise_generic_exception(**kwargs): + raise Exception("Unexpected failure") + + stubber.client.create_db_cluster = raise_generic_exception + with pytest.raises(RuntimeError, match="Unexpected error creating DB cluster 'fail-cluster'"): + create_db_cluster(stubber.client, "fail-cluster") diff --git a/python/example_code/neptune/tests/test_create_db_instance.py b/python/example_code/neptune/tests/test_create_db_instance.py new file mode 100644 index 00000000000..a2ad6f5ab62 --- /dev/null +++ b/python/example_code/neptune/tests/test_create_db_instance.py @@ -0,0 +1,51 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import boto3 +from botocore.exceptions import ClientError +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import create_db_instance + +class DummyWaiter: + def __init__(self, name): + self.name = name + def wait(self, **kwargs): + return None + +def test_create_db_instance(): + boto_client = boto3.client("neptune") + stubber = Neptune(boto_client) + + instance_id = "my-instance" + cluster_id = "my-cluster" + + stubber.client.get_waiter = lambda name: DummyWaiter(name) + + stubber.stub_create_db_instance(instance_id, cluster_id) + result = create_db_instance(stubber.client, instance_id, cluster_id) + assert result == instance_id + + stubber.stubber.add_response( + "create_db_instance", + {"DBInstance": {}}, + expected_params={ + "DBInstanceIdentifier": "no-id-instance", + "DBInstanceClass": "db.r5.large", + "Engine": "neptune", + "DBClusterIdentifier": cluster_id + } + ) + with pytest.raises(RuntimeError, match="no ID returned"): + create_db_instance(stubber.client, "no-id-instance", cluster_id) + + stubber.stub_create_db_instance("fail-instance", cluster_id, error_code="AccessDenied") + with pytest.raises(ClientError) as e: + create_db_instance(stubber.client, "fail-instance", cluster_id) + assert "AccessDenied error" in str(e.value) + + def broken_call(**kwargs): + raise Exception("DB is on fire") + stubber.client.create_db_instance = broken_call + with pytest.raises(RuntimeError, match="Unexpected error creating DB instance 'boom-instance'"): + create_db_instance(stubber.client, "boom-instance", cluster_id) diff --git a/python/example_code/neptune/tests/test_create_subnet_group.py b/python/example_code/neptune/tests/test_create_subnet_group.py new file mode 100644 index 00000000000..24882da28f7 --- /dev/null +++ b/python/example_code/neptune/tests/test_create_subnet_group.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +from unittest.mock import patch +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import create_subnet_group + +@patch("example_code.neptune.neptune_scenario.get_subnet_ids") +@patch("example_code.neptune.neptune_scenario.get_default_vpc_id") +def test_create_subnet_group(mock_get_vpc, mock_get_subnets): + mock_get_vpc.return_value = "vpc-1234" + mock_get_subnets.return_value = ["subnet-1", "subnet-2"] + + boto_client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(boto_client) + + stubber.stub_create_db_subnet_group( + group_name="test-group", + subnet_ids=["subnet-1", "subnet-2"], + group_arn="arn:aws:neptune:us-east-1:123456789012:subnet-group:test-group", + description="My Neptune subnet group", + tags=[{"Key": "Environment", "Value": "Dev"}] + ) + + create_subnet_group(boto_client, "test-group") + diff --git a/python/example_code/neptune/tests/test_delete_db_cluster.py b/python/example_code/neptune/tests/test_delete_db_cluster.py new file mode 100644 index 00000000000..bccec912f81 --- /dev/null +++ b/python/example_code/neptune/tests/test_delete_db_cluster.py @@ -0,0 +1,34 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import boto3 +from botocore.exceptions import ClientError +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import delete_db_cluster + +def test_delete_db_cluster_success_and_clienterror(): + neptune_client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(neptune_client) + + stubber.stub_delete_db_cluster("test-cluster") + delete_db_cluster(neptune_client, "test-cluster") # Should not raise + + stubber.stub_delete_db_cluster("unauthorized-cluster", error_code="AccessDenied") + + with pytest.raises(ClientError) as exc_info: + delete_db_cluster(neptune_client, "unauthorized-cluster") + + assert "AccessDenied" in str(exc_info.value) + +def test_delete_db_cluster_unexpected_exception(monkeypatch): + client = boto3.client("neptune", region_name="us-east-1") + + def raise_unexpected_error(**kwargs): + raise Exception("Unexpected error") + + monkeypatch.setattr(client, "delete_db_cluster", raise_unexpected_error) + + with pytest.raises(Exception, match="Unexpected error"): + delete_db_cluster(client, "error-cluster") + diff --git a/python/example_code/neptune/tests/test_delete_db_instance.py b/python/example_code/neptune/tests/test_delete_db_instance.py new file mode 100644 index 00000000000..03b16ef6cdf --- /dev/null +++ b/python/example_code/neptune/tests/test_delete_db_instance.py @@ -0,0 +1,33 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from botocore.exceptions import ClientError +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import delete_db_instance + +def test_delete_db_instance_success(): + import boto3 + neptune_client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(neptune_client) + + instance_id = "instance-1" + stubber.stub_delete_db_instance(instance_id, statuses=["deleting", "deleted"]) + + delete_db_instance(neptune_client, instance_id) + stubber.stubber.assert_no_pending_responses() + + +def test_delete_db_instance_client_error(): + import boto3 + neptune_client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(neptune_client) + + instance_id = "bad-instance" + stubber.stub_delete_db_instance(instance_id, error_code="InvalidDBInstanceState") + + with pytest.raises(ClientError) as exc_info: + delete_db_instance(neptune_client, instance_id) + + assert "InvalidDBInstanceState" in str(exc_info.value) + stubber.stubber.assert_no_pending_responses() diff --git a/python/example_code/neptune/tests/test_delete_db_subnet_group.py b/python/example_code/neptune/tests/test_delete_db_subnet_group.py new file mode 100644 index 00000000000..89fbdc8119d --- /dev/null +++ b/python/example_code/neptune/tests/test_delete_db_subnet_group.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import boto3 +from botocore.exceptions import ClientError +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import delete_db_subnet_group + +def test_delete_db_subnet_group(): + boto_client = boto3.client("neptune", region_name="us-east-1") + stubber = Neptune(boto_client) + + stubber.stub_delete_db_subnet_group("my-subnet-group") + delete_db_subnet_group(stubber.client, "my-subnet-group") + + stubber.stub_delete_db_subnet_group( + "unauthorized-subnet", + error_code="AccessDenied" + ) + + with pytest.raises(ClientError) as exc_info: + delete_db_subnet_group(stubber.client, "unauthorized-subnet") + + assert "AccessDenied" in str(exc_info.value) diff --git a/python/example_code/neptune/tests/test_describe_db_clusters.py b/python/example_code/neptune/tests/test_describe_db_clusters.py new file mode 100644 index 00000000000..0ca840b0bf8 --- /dev/null +++ b/python/example_code/neptune/tests/test_describe_db_clusters.py @@ -0,0 +1,78 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import pytest +from botocore.exceptions import ClientError +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import describe_db_clusters + + +@pytest.fixture +def neptune_client(): + return boto3.client("neptune", region_name="us-east-1") + +def test_cluster_found_and_prints_info(neptune_client): + stubber = Neptune(neptune_client) + stubber.stubber.activate() + stubber.stubber.add_response( + "describe_db_clusters", + { + "DBClusters": [ + { + "DBClusterIdentifier": "test-cluster", + "Status": "available", + "Engine": "neptune", + "EngineVersion": "1.2.0.0", + "Endpoint": "test-endpoint", + "ReaderEndpoint": "reader-endpoint", + "AvailabilityZones": ["us-east-1a"], + "DBSubnetGroup": "default", + "VpcSecurityGroups": [{"VpcSecurityGroupId": "sg-12345"}], + "StorageEncrypted": True, + "IAMDatabaseAuthenticationEnabled": True, + "BackupRetentionPeriod": 7, + "PreferredBackupWindow": "07:00-09:00", + "PreferredMaintenanceWindow": "sun:05:00-sun:09:00", + } + ] + }, + expected_params={"DBClusterIdentifier": "test-cluster"}, + ) + + describe_db_clusters(neptune_client, "test-cluster") + stubber.stubber.deactivate() + +def test_cluster_not_found_raises_client_error(neptune_client): + stubber = Neptune(neptune_client) + stubber.stubber.activate() + + stubber.stubber.add_response( + "describe_db_clusters", + {"DBClusters": []}, + expected_params={"DBClusterIdentifier": "test-cluster"}, + ) + + with pytest.raises(ClientError) as excinfo: + describe_db_clusters(neptune_client, "test-cluster") + + assert excinfo.value.response["Error"]["Code"] == "DBClusterNotFound" + stubber.stubber.deactivate() + + +def test_client_error_from_paginate_is_propagated(neptune_client): + stubber = Neptune(neptune_client) + stubber.stubber.activate() + + stubber.stub_describe_db_cluster_status( + cluster_id="test-cluster", + statuses=[], + error_code="AccessDeniedException", + ) + + with pytest.raises(ClientError) as excinfo: + describe_db_clusters(neptune_client, "test-cluster") + + assert excinfo.value.response["Error"]["Code"] == "AccessDeniedException" + stubber.stubber.deactivate() + diff --git a/python/example_code/neptune/tests/test_hello.py b/python/example_code/neptune/tests/test_hello.py new file mode 100644 index 00000000000..3bb0486234d --- /dev/null +++ b/python/example_code/neptune/tests/test_hello.py @@ -0,0 +1,34 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import pytest +from botocore.stub import Stubber +from example_code.neptune.hello_neptune import describe_db_clusters + +@pytest.fixture +def neptune_client_stub(): + client = boto3.client("neptune", region_name="us-east-1") + stubber = Stubber(client) + stubber.activate() + yield client, stubber + stubber.deactivate() + + +def test_describe_db_clusters_with_stubber_single_page(neptune_client_stub, capsys): + client, stubber = neptune_client_stub + + stubber.add_response("describe_db_clusters", { + "DBClusters": [ + {"DBClusterIdentifier": "my-test-cluster", "Status": "available"}, + {"DBClusterIdentifier": "my-second-cluster", "Status": "modifying"} + ] + }) + + describe_db_clusters(client) + captured = capsys.readouterr() + + assert "my-test-cluster" in captured.out + assert "available" in captured.out + assert "my-second-cluster" in captured.out + assert "modifying" in captured.out diff --git a/python/example_code/neptune/tests/test_start_db_cluster.py b/python/example_code/neptune/tests/test_start_db_cluster.py new file mode 100644 index 00000000000..961ded6b024 --- /dev/null +++ b/python/example_code/neptune/tests/test_start_db_cluster.py @@ -0,0 +1,36 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import start_db_cluster + +def test_start_db_cluster_success(monkeypatch): + cluster_id = "my-cluster" + client = boto3.client("neptune", region_name="us-east-1") + neptune = Neptune(client) + + neptune.stubber.add_response( + "start_db_cluster", + {"DBCluster": {"DBClusterIdentifier": cluster_id}}, + {"DBClusterIdentifier": cluster_id} + ) + + statuses = ["starting"] * 5 + ["available"] + for status in statuses: + neptune.stubber.add_response( + "describe_db_clusters", + {"DBClusters": [{"DBClusterIdentifier": cluster_id, "Status": status}]}, + {"DBClusterIdentifier": cluster_id} + ) + + monkeypatch.setattr("example_code.neptune.neptune_scenario.time.sleep", lambda _: None) + + monkeypatch.setattr("example_code.neptune.neptune_scenario.POLL_INTERVAL_SECONDS", 0.01) + monkeypatch.setattr("example_code.neptune.neptune_scenario.TIMEOUT_SECONDS", 1) + + start_db_cluster(client, cluster_id) + neptune.stubber.deactivate() + + + diff --git a/python/example_code/neptune/tests/test_stop_db_cluster.py b/python/example_code/neptune/tests/test_stop_db_cluster.py new file mode 100644 index 00000000000..b2afca96ece --- /dev/null +++ b/python/example_code/neptune/tests/test_stop_db_cluster.py @@ -0,0 +1,46 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import pytest +from test_tools.neptune_stubber import Neptune +from example_code.neptune.neptune_scenario import stop_db_cluster + +@pytest.fixture +def neptune_client(): + return boto3.client('neptune', region_name='us-west-2') + +def test_stop_db_cluster_with_stubbed_responses(neptune_client): + cluster_id = "timeout-cluster" + neptune = Neptune(neptune_client) + + neptune.stubber.add_response( + "stop_db_cluster", + {"DBCluster": {"DBClusterIdentifier": cluster_id}}, + {"DBClusterIdentifier": cluster_id} + ) + + for _ in range(9): + neptune.stubber.add_response( + "describe_db_clusters", + { + "DBClusters": [ + {"DBClusterIdentifier": cluster_id, "Status": "stopping"} + ] + }, + {"DBClusterIdentifier": cluster_id} + ) + + neptune.stubber.add_response( + "describe_db_clusters", + { + "DBClusters": [ + {"DBClusterIdentifier": cluster_id, "Status": "stopped"} + ] + }, + {"DBClusterIdentifier": cluster_id} + ) + + stop_db_cluster(neptune.client, cluster_id) # Just call the function + + neptune.stubber.deactivate() diff --git a/python/test_tools/neptune_data_stubber.py b/python/test_tools/neptune_data_stubber.py new file mode 100644 index 00000000000..6cc2f99006b --- /dev/null +++ b/python/test_tools/neptune_data_stubber.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import io +from botocore.stub import Stubber +from botocore.response import StreamingBody +from botocore.config import Config +import json + + +class NeptuneDateStubber: + def __init__(self): + """ + Create NeptuneData client and stubber with minimal retry config for testing. + """ + config = Config(connect_timeout=10, read_timeout=30, retries={'max_attempts': 3}) + self.client = boto3.client("neptunedata", config=config, endpoint_url="http://fake-endpoint:8182") + self.stubber = Stubber(self.client) + + def _make_streaming_body(self, data_str: str): + data_bytes = data_str.encode("utf-8") + return StreamingBody(io.BytesIO(data_bytes), len(data_bytes)) + + def add_execute_gremlin_explain_query_stub(self, gremlin_query, response_payload): + expected_params = {"gremlinQuery": gremlin_query} + response = {"output": self._make_streaming_body(response_payload)} + self.stubber.add_response("execute_gremlin_explain_query", response, expected_params) + + def add_execute_gremlin_profile_query_stub(self, gremlin_query, response_payload): + expected_params = {"gremlinQuery": gremlin_query} + response = {"output": self._make_streaming_body(response_payload)} + self.stubber.add_response("execute_gremlin_profile_query", response, expected_params) + + def add_execute_gremlin_query_stub(self, gremlin_query, response_dict): + expected_params = {"gremlinQuery": gremlin_query} + # The real code expects response['result'] as a normal dict/json, not StreamingBody + self.stubber.add_response("execute_gremlin_query", response_dict, expected_params) + + def add_execute_open_cypher_query_stub(self, open_cypher_query, parameters=None, results_dict=None): + expected_params = {"openCypherQuery": open_cypher_query} + if parameters: + expected_params["parameters"] = parameters if isinstance(parameters, str) else json.dumps(parameters) + if results_dict is None: + results_dict = {} + self.stubber.add_response("execute_open_cypher_query", results_dict, expected_params) + + def add_execute_open_cypher_explain_query_stub(self, open_cypher_query, explain_mode, results_payload): + expected_params = {"openCypherQuery": open_cypher_query, "explainMode": explain_mode} + response = {"results": self._make_streaming_body(results_payload)} + self.stubber.add_response("execute_open_cypher_explain_query", response, expected_params) + + def activate(self): + self.stubber.activate() + + def deactivate(self): + self.stubber.deactivate() + + def get_client(self): + return self.client + + + diff --git a/python/test_tools/neptune_graph_stubber.py b/python/test_tools/neptune_graph_stubber.py new file mode 100644 index 00000000000..ce0b39fae31 --- /dev/null +++ b/python/test_tools/neptune_graph_stubber.py @@ -0,0 +1,78 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import io +from botocore.stub import Stubber +from botocore.response import StreamingBody +from botocore.config import Config + +GRAPH_ID = "my-graph-id" +GRAPH_NAME = "my-test-graph" + +class NeptuneGraphStubber: + def __init__(self): + """ + Create NeptuneGraph client and stubber with minimal retry config for testing. + """ + config = Config(retries={"total_max_attempts": 1, "mode": "standard"}, read_timeout=None) + self.client = boto3.client("neptune-graph", config=config) + self.stubber = Stubber(self.client) + + def add_execute_query_stub(self, graph_id, query_string, language, explain_mode=None, parameters=None): + """ + Add stub response for execute_query call matching parameters including optional explainMode and parameters. + """ + expected_params = { + "graphIdentifier": graph_id, + "queryString": query_string, + "language": language, + } + if explain_mode is not None: + expected_params["explainMode"] = explain_mode + if parameters is not None: + expected_params["parameters"] = parameters + + # Example JSON payload response the service expects + payload_bytes = b'{"results": [{"n": {"code": "ANC"}}]}' + response_body = StreamingBody(io.BytesIO(payload_bytes), len(payload_bytes)) + response = {"payload": response_body} + + self.stubber.add_response("execute_query", response, expected_params) + + def add_create_graph_stub(self, graph_name, memory=16): + """ + Add stub response for create_graph call matching graphName and provisionedMemory parameters. + """ + expected_params = { + "graphName": graph_name, + "provisionedMemory": memory + } + response = { + "id": "test-graph-id", # Required field in response + "name": graph_name, + "arn": f"arn:aws:neptune-graph:us-east-1:123456789012:graph/{graph_name}", + "endpoint": f"http://{graph_name}.cluster-neptune.amazonaws.com" + } + self.stubber.add_response("create_graph", response, expected_params) + + def activate(self): + """ + Activate the stubber. + """ + self.stubber.activate() + + def deactivate(self): + """ + Deactivate the stubber. + """ + self.stubber.deactivate() + + def get_client(self): + """ + Return the stubbed NeptuneGraph client. + """ + return self.client + + + diff --git a/python/test_tools/neptune_stubber.py b/python/test_tools/neptune_stubber.py new file mode 100644 index 00000000000..5d522c53b94 --- /dev/null +++ b/python/test_tools/neptune_stubber.py @@ -0,0 +1,287 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from botocore.stub import Stubber + +class Neptune: + def __init__(self, client): + self.client = client + self.stubber = Stubber(client) + self.stubber.activate() + + def stub_create_db_subnet_group(self, group_name, subnet_ids, group_arn=None, error_code=None, description=None, tags=None): + expected_params = { + "DBSubnetGroupName": group_name, + "DBSubnetGroupDescription": description or f"Subnet group for {group_name}", + "SubnetIds": subnet_ids, + } + if tags: + expected_params["Tags"] = tags + + if error_code: + self.stubber.add_client_error( + "create_db_subnet_group", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params + ) + else: + response = { + "DBSubnetGroup": { + "DBSubnetGroupName": group_name, + } + } + if group_arn: + response["DBSubnetGroup"]["DBSubnetGroupArn"] = group_arn + + self.stubber.add_response( + "create_db_subnet_group", + response, + expected_params + ) + + def stub_create_db_cluster(self, cluster_id=None, error_code=None, + backup_retention_period=1, deletion_protection=False, engine='neptune'): + expected_params = { + "DBClusterIdentifier": cluster_id, + "BackupRetentionPeriod": backup_retention_period, + "DeletionProtection": deletion_protection, + "Engine": engine + } + + if error_code: + self.stubber.add_client_error( + "create_db_cluster", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params + ) + else: + response = { + "DBCluster": { + "DBClusterIdentifier": cluster_id, + } + } + self.stubber.add_response( + "create_db_cluster", + response, + expected_params + ) + + def stub_create_db_instance(self, instance_id, cluster_id, error_code=None): + expected_params = { + "DBInstanceIdentifier": instance_id, + "DBInstanceClass": "db.r5.large", + "Engine": "neptune", + "DBClusterIdentifier": cluster_id + } + + if error_code: + self.stubber.add_client_error( + "create_db_instance", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params + ) + else: + response = { + "DBInstance": { + "DBInstanceIdentifier": instance_id + } + } + self.stubber.add_response( + "create_db_instance", + response, + expected_params + ) + + def stub_describe_db_instance_status(self, instance_id, statuses, error_code=None): + pages = [{"DBInstances": [{"DBInstanceIdentifier": instance_id, "DBInstanceStatus": status}]} for status in statuses] + + if error_code: + self.stubber.add_client_error( + "describe_db_instances", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params={"DBInstanceIdentifier": instance_id} + ) + else: + for page in pages: + self.stubber.add_response( + "describe_db_instances", + page, + expected_params={"DBInstanceIdentifier": instance_id} + ) + + def stub_stop_db_cluster(self, cluster_id, error_code=None): + expected_params = {"DBClusterIdentifier": cluster_id} + if error_code: + self.stubber.add_client_error( + "stop_db_cluster", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params, + ) + else: + self.stubber.add_response( + "stop_db_cluster", + {"DBCluster": {"DBClusterIdentifier": cluster_id}}, + expected_params + ) + + def stub_describe_db_cluster_status(self, cluster_id, statuses, error_code=None): + expected_params = {"DBClusterIdentifier": cluster_id} + + if error_code: + self.stubber.add_client_error( + "describe_db_clusters", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params + ) + else: + for status in statuses: + response = { + "DBClusters": [{ + "DBClusterIdentifier": cluster_id, + "Status": status + }] + } + self.stubber.add_response( + "describe_db_clusters", + response, + expected_params + ) + + def stub_start_db_cluster(self, cluster_id, statuses, error_code=None): + start_params = {"DBClusterIdentifier": cluster_id} + + if error_code: + self.stubber.add_client_error( + "start_db_cluster", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=start_params, + ) + return + + self.stubber.add_response( + "start_db_cluster", + {}, + expected_params=start_params, + ) + + describe_params = {"DBClusterIdentifier": cluster_id} + for status in statuses: + response = { + "DBClusters": [{ + "DBClusterIdentifier": cluster_id, + "Status": status + }] + } + self.stubber.add_response( + "describe_db_clusters", + response, + expected_params=describe_params, + ) + + def stub_delete_db_instance(self, instance_id, statuses=None, error_code=None): + expected_params = { + "DBInstanceIdentifier": instance_id, + "SkipFinalSnapshot": True, + } + + if error_code: + self.stubber.add_client_error( + "delete_db_instance", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params, + ) + return + + self.stubber.add_response( + "delete_db_instance", + {}, + expected_params + ) + + if statuses: + for status in statuses: + response = { + "DBInstances": [ + { + "DBInstanceIdentifier": instance_id, + "DBInstanceStatus": status + } + ] + } + self.stubber.add_response( + "describe_db_instances", + response, + expected_params={"DBInstanceIdentifier": instance_id} + ) + + def stub_delete_db_cluster(self, cluster_id, error_code=None): + expected_params = { + "DBClusterIdentifier": cluster_id, + "SkipFinalSnapshot": True, + } + + if error_code: + self.stubber.add_client_error( + "delete_db_cluster", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params, + ) + else: + self.stubber.add_response( + "delete_db_cluster", + {}, + expected_params + ) + + def stub_describe_all_db_clusters(self, pages, error_code=None): + """ + Stub for describe_db_clusters using a paginator simulation. + :param pages: List of pages, where each page is a list of DBClusters. + Example: [[{...}], [{...}]] simulates 2 pages. + """ + if error_code: + self.stubber.add_client_error( + "describe_db_clusters", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params={} + ) + else: + for clusters in pages: + response = { + "DBClusters": clusters + } + self.stubber.add_response( + "describe_db_clusters", + response, + expected_params={} + ) + + def stub_delete_db_subnet_group(self, group_name, error_code=None): + expected_params = { + "DBSubnetGroupName": group_name + } + + if error_code: + self.stubber.add_client_error( + "delete_db_subnet_group", + service_error_code=error_code, + service_message=f"{error_code} error", + expected_params=expected_params, + ) + else: + self.stubber.add_response( + "delete_db_subnet_group", + {}, + expected_params + ) + diff --git a/scenarios/basics/neptune/SPECIFICATION.md b/scenarios/basics/neptune/SPECIFICATION.md index 7575ebc8be1..ce4627434da 100644 --- a/scenarios/basics/neptune/SPECIFICATION.md +++ b/scenarios/basics/neptune/SPECIFICATION.md @@ -6,42 +6,13 @@ It demonstrates various tasks such as creating a Neptune DB Subnet Group, creati Finally this scenario demonstrates how to clean up resources. Its purpose is to demonstrate how to get up and running with Amazon Neptune and an AWS SDK. -## Is using NeptuneClient worth while (Amazon Bedrock results) - -Here is more context on when it's a good idea to use the `NeptuneAsyncClient`: - -1. **Dynamic Resource Provisioning**: The `NeptuneAsyncClient` can be particularly useful when you need to dynamically create, update, or delete Neptune resources as part of your application's functionality. This could be useful in use cases such as: - - - **Multi-tenant Applications**: If you're building a SaaS application that needs to provision Neptune instances on-demand, the `NeptuneAsyncClient` can help you automate this process programmatically. - - **Ephemeral Environments**: When you need to spin up and tear down Neptune resources as part of your CI/CD pipeline or within a Lambda environments, the `NeptuneAsyncClient` can streamline this process. - - **Scaling and Elasticity**: If your application needs to scale Neptune resources up or down based on demand, the `NeptuneAsyncClient` can help you manage these changes dynamically. - -2. **Integrations and Workflow Automation**: The `NeptuneAsyncClient` can be beneficial when you need to integrate Neptune provisioning and management into larger, automated workflows. For example: - - - **DevOps Tooling**: You can use the `NeptuneAsyncClient` as part of your infrastructure-as-code (IaC) tooling, such as building custom scripts that can provision Neptune resources on-demand. - - **Serverless Architectures**: When deploying serverless applications that rely on Neptune, the `NeptuneAsyncClient` can help you manage the Neptune components of your serverless stack. - - -3. **Rapid Prototyping and Experimentation**: The programmatic nature of the `NeptuneAsyncClient` can be beneficial when you need to quickly set up and tear down Neptune resources for prototyping, testing, or experimentation purposes. This can be particularly useful for: - - - **Proof-of-Concepts**: When validating ideas or testing new features that require a Neptune database, the `NeptuneAsyncClient` can help you provision the necessary resources with minimal overhead. - - **Performance Testing**: If you need to stress-test your Neptune-powered application, the NeptuneAsyncClient can help you programmatically create and manage the required test environments. - - **Data Migrations**: When migrating data between Neptune instances or across AWS Regions, the NeptuneAsyncClient can streamline the process of provisioning the necessary resources. - -The key advantage of the `NeptuneAsyncClient` is its ability to provide fine-grained, programmatic control over Neptune resources. This can be particularly valuable in dynamic, automated, or rapidly changing environments where the flexibility and programmability of the `NeptuneAsyncClient` can help streamline your application's Neptune-related infrastructure management. - -### Use Case Recommendation - -- Infrastructure as code (IaC): Prefer CDK, CloudFormation, or Terraform -- Dynamic provisioning in app - Use NeptuneAsyncClient -- Internal tooling or automation - Use NeptuneAsyncClient - - Manual ad hoc cluster setup - Use CLI or SDK (sync/async) - ## Resources This Basics scenario does not require any additional AWS resources. ## Hello Amazon Neptune -This program is intended for users not familiar with Amazon Neptune to easily get up and running. The program invokes `describeDBClustersPaginator`to iterate through subnet groups. +This program is intended for users not familiar with Amazon Neptune to easily get up and running. The program invokes `describeDBClustersPaginator`to iterate through subnet groups. ' + +Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. ## Basics Scenario Program Flow The Amazon Neptune Basics scenario executes the following operations. @@ -61,15 +32,15 @@ The Amazon Neptune Basics scenario executes the following operations. 4. **Check the status of the Neptune DB Instance**: - Description: Check the status of the DB instance by invoking `describeDBInstances`. Poll the instance until it reaches an `availbale`state. - - Exception Handling: This operatioin handles a `CompletionException`. If thrown, display the message and end the program. + - Exception Handling: This operatioin handles a `ResourceNotFoundException`. If thrown, display the message and end the program. 5. **Show Neptune Cluster details**: - Description: Shows the details of the cluster by invoking `describeDBClusters`. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + - Exception Handling: Check to see if a `DBClusterNotFound` is thrown. If so, display the message and end the program. 6. **Stop the Cluster**: - Description: Stop the cluster by invoking `stopDBCluster`. Poll the cluster until it reaches a `stopped`state. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + - Exception Handling: Check to see if a `DBClusterNotFound` is thrown. If so, display the message and end the program. 7. **Start the cluster**: - Description: Start the cluster by invoking `startBCluster`. Poll the cluster until it reaches an `available`state. @@ -78,7 +49,7 @@ The Amazon Neptune Basics scenario executes the following operations. 8. **Delete the Neptune Assets**: - Description: Delete the various resources. - - Exception Handling: Check to see if an `ResourceNotFoundException` is thrown. If so, display the message and end the program. + - Exception Handling: Each call can throw a different exception. The `delete_db_instance` method will handle `DBInstanceNotFoundFault`. The `delete_db_cluster` method will handle `DBClusterNotFoundFault`and the ` delete_db_subnet_group` method will handle `DBSubnetGroupNotFoundFault`. ### Program execution The following shows the output of the Amazon Neptune Basics scenario. @@ -275,15 +246,26 @@ The following table describes the metadata used in this Basics Scenario. The met |`createDBSubnetGroup` | neptune_CreateDBSubnetGroup | |`createDBCluster` | neptune_CreateDBCluster | |`createDBInstance` | neptune_CreateDBInstance | -|`describeDBInstances ` | neptune_DescribeDBInstances | +|`describeDBInstances` | neptune_DescribeDBInstances | |`describeDBClusters` | neptune_DescribeDBClusters | | `stopDBCluster` | neptune_StopDBCluster | -|`startDBCluster ` | neptune_StartDBCluster | -|`deleteDBInstance ` | neptune_DeleteDBInstance | -| `deleteDBCluster` | neptune_DeleteDBCluster | -| `deleteDBSubnetGroup `| neptune_DeleteDBSubnetGroup | -| `scenario` | neptune_Scenario | -| `hello` | neptune_Hello | - - +|`startDBCluster` | neptune_StartDBCluster | +|`deleteDBInstance` | neptune_DeleteDBInstance | +|`deleteDBCluster` | neptune_DeleteDBCluster | +|`deleteDBSubnetGroup` | neptune_DeleteDBSubnetGroup | +|`scenario` | neptune_Scenario | +|`hello` | neptune_Hello | + +### Additional SOS Tags +We will add additional code examples to the AWS Code Library. These code examples were created by the SME. These APIs cannot be used in the main scenario because you must run them from within the same VPC as the cluster. There is no console access. However, we will still add them to the AWS Code Library. + +This table decribes the SOS tags for NeptunedataClient and NeptuneGraphClient. + +| action | metadata key | +|-------------------------------|------------------------------------- | +|`executeGremlinProfileQuery` | neptune_ExecuteGremlinProfileQuery | +|`executeGremlinQuery` | neptune_ExecuteGremlinQuery | +|`executeOpenCypherExplainQuery`| neptune_ExecuteOpenCypherExplainQuery | +|`createGraph ` | neptune_CreateGraph: | +|`executeQuery` | neptune_ExecuteQuery |