From 2139d5fc18ae18738ad3280d1b5b9d7d6bd88f15 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 09:20:48 -0500 Subject: [PATCH 001/144] add Entity Resolution files --- .../example_code/entityresolution/.gitignore | 38 ++ javav2/example_code/entityresolution/pom.xml | 91 +++++ .../entity/scenario/EntityResActions.java | 376 ++++++++++++++++++ .../entity/scenario/EntityResScenario.java | 261 ++++++++++++ .../src/main/resources/glue.yaml | 240 +++++++++++ 5 files changed, 1006 insertions(+) create mode 100644 javav2/example_code/entityresolution/.gitignore create mode 100644 javav2/example_code/entityresolution/pom.xml create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java create mode 100644 javav2/example_code/entityresolution/src/main/resources/glue.yaml diff --git a/javav2/example_code/entityresolution/.gitignore b/javav2/example_code/entityresolution/.gitignore new file mode 100644 index 00000000000..5ff6309b719 --- /dev/null +++ b/javav2/example_code/entityresolution/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml new file mode 100644 index 00000000000..f710bd02f78 --- /dev/null +++ b/javav2/example_code/entityresolution/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + org.example + entityresolution + 1.0-SNAPSHOT + + + UTF-8 + 17 + 17 + 17 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + IntegrationTest + + + + + + + + software.amazon.awssdk + bom + 2.29.45 + pom + import + + + + + + org.junit.jupiter + junit-jupiter-api + 5.9.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + software.amazon.awssdk + secretsmanager + + + com.google.code.gson + gson + 2.10.1 + + + org.junit.platform + junit-platform-commons + 1.9.2 + + + org.junit.platform + junit-platform-launcher + 1.9.2 + test + + + software.amazon.awssdk + entityresolution + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + cloudformation + + + + \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java new file mode 100644 index 00000000000..b2dfda3e4f6 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -0,0 +1,376 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity.scenario; + +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.entityresolution.EntityResolutionClient; +import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; + +import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowResponse; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; +import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; +import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; +import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; +import software.amazon.awssdk.services.entityresolution.model.InputSource; +import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; +import software.amazon.awssdk.services.entityresolution.model.OutputSource; +import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; +import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; +import software.amazon.awssdk.services.entityresolution.model.ResolutionType; +import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; +import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobResponse; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; + +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class EntityResActions { + + private EntityResolutionClient resolutionClient; + + private static EntityResolutionAsyncClient entityResolutionAsyncClient; + + private static S3AsyncClient s3AsyncClient; + + public static EntityResolutionAsyncClient getResolutionAsyncClient() { + if (entityResolutionAsyncClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return entityResolutionAsyncClient; + } + + + public static S3AsyncClient getS3AsyncClient() { + if (s3AsyncClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + s3AsyncClient = S3AsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return s3AsyncClient; + } + + // snippet-start:[entityres.java2_delete_matching_workflow.main] + /** + * Asynchronously deletes a workflow with the specified name. + * + * @param workflowName the name of the workflow to be deleted + * @return a {@link CompletableFuture} that completes when the workflow has been deleted + * @throws RuntimeException if the deletion of the workflow fails + */ + public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { + DeleteMatchingWorkflowRequest request = DeleteMatchingWorkflowRequest.builder() + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().deleteMatchingWorkflow(request) + .thenAccept(response -> { + // No response object, just log success + }) + .exceptionally(exception -> { + throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); + }); + } + // snippet-end:[entityres.java2_delete_matching_workflow.main] + + // snippet-start:[entityres.java2_create_schema.main] + /** + * Creates a schema mapping asynchronously. + * + * @param schemaName the name of the schema to create the mapping for + * @return a {@link CompletableFuture} that represents the asynchronous creation of the schema mapping + */ + public CompletableFuture createSchemaMappingAsync(String schemaName) { + List schemaAttributes = List.of( + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type("UNIQUE_ID").build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type("STRING").build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type("STRING").build() + ); + + CreateSchemaMappingRequest request = CreateSchemaMappingRequest.builder() + .schemaName(schemaName) + .mappedInputFields(schemaAttributes) + .build(); + + return getResolutionAsyncClient().createSchemaMapping(request) + .whenComplete((response, exception) -> { + if (response != null) { + System.out.println("Schema Mapping Created Successfully!"); + } else { + throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); + } + }); + } + // snippet-end:[entityres.java2_create_schema.main] + + // snippet-start:[entityres.java2_get_schema_mapping.main] + /** + * Retrieves the schema mapping asynchronously. + * + * @param schemaName the name of the schema to retrieve the mapping for + * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation is complete + * @throws RuntimeException if the schema mapping retrieval fails + */ + public CompletableFuture getSchemaMappingAsync(String schemaName) { + GetSchemaMappingRequest mappingRequest = GetSchemaMappingRequest.builder() + .schemaName(schemaName) + .build(); + + return getResolutionAsyncClient().getSchemaMapping(mappingRequest) + .whenComplete((response, exception) -> { + if (response != null) { + response.mappedInputFields().forEach(attribute -> + System.out.println("Attribute Name: " + attribute.fieldName() + + ", Attribute Type: " + attribute.type().toString())); + } else { + throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); + } + }); + } + // snippet-end:[entityres.java2_get_schema_mapping.main] + + // snippet-start:[entityres.java2_get_job.main] + /** + * Asynchronously retrieves a matching job based on the provided job ID and workflow name. + * + * @param jobId the ID of the job to retrieve + * @param workflowName the name of the workflow associated with the job + * @return a {@link CompletableFuture} that completes when the job information is available or an exception occurs + */ + public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { + GetMatchingJobRequest request = GetMatchingJobRequest.builder() + .jobId(jobId) + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().getMatchingJob(request) + .thenAccept(response -> { + System.out.println("Job status: " + response.status()); + System.out.println("Job details: " + response.toString()); + }) + .exceptionally(ex -> { + throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); + }); + } + // snippet-end:[entityres.java2_get_job.main] + + // snippet-start:[entityres.java2_start_job.main] + /** + * Starts a matching job asynchronously for the specified workflow name. + * + * @param workflowName the name of the workflow for which to start the matching job + * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty string if the operation fails + */ + public CompletableFuture startMatchingJobAsync(String workflowName) { + StartMatchingJobRequest jobRequest = StartMatchingJobRequest.builder() + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().startMatchingJob(jobRequest) + .whenComplete((response, exception) -> { + if (response != null) { + // Get the job ID from the response + String jobId = response.jobId(); + System.out.println("Job ID: " + jobId); + } else { + // Handle the exception if the response is null + throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response != null ? response.jobId() : ""); + } + // snippet-end:[entityres.java2_start_job.main] + + // snippet-start:[entityres.java2_check_matching_workflow.main] + /** + * Checks the status of a workflow asynchronously. + * + * @param jobId the ID of the job to check + * @param workflowName the name of the workflow to check + * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed successfully + */ + public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { + GetMatchingJobRequest request = GetMatchingJobRequest.builder() + .jobId(jobId) + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().getMatchingJob(request) + .thenApply(response -> { + System.out.println("\nJob status: " + response.status()); + return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); + }) + .exceptionally(exception -> { + System.out.println("Error checking workflow status: " + exception.getMessage()); + return false; + }); + } + // snippet-end:[entityres.java2_check_matching_workflow.main] + + // snippet-start:[entityres.java2_create_matching_workflow.main] + /** + * Creates an asynchronous CompletableFuture to manage the creation of a matching workflow. + * + * @param roleARN the AWS IAM role ARN to be used for the workflow execution + * @param workflowName the name of the workflow to be created + * @param outputBucket the S3 bucket path where the workflow output will be stored + * @param inputGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source + * @param schemaName the name of the schema to be used for the input source + * @return a CompletableFuture that, when completed, will return the ARN of the created workflow + */ + public CompletableFuture createMatchingWorkflowAsync(String roleARN, String workflowName, String outputBucket, String inputGlueTableArn, String schemaName) { + InputSource inputSource = InputSource.builder() + .inputSourceARN(inputGlueTableArn) + .schemaName(schemaName) + .build(); + + OutputAttribute outputAttribute = OutputAttribute.builder() + .name("id") + .build(); + + OutputSource outputSource = OutputSource.builder() + .outputS3Path(outputBucket) + .output(outputAttribute) + .build(); + + ResolutionTechniques type = ResolutionTechniques.builder() + .resolutionType(ResolutionType.ML_MATCHING) + .build(); + + CreateMatchingWorkflowRequest workflowRequest = CreateMatchingWorkflowRequest.builder() + .roleArn(roleARN) + .description("Created by using the AWS SDK for Java") + .workflowName(workflowName) + .inputSourceConfig(List.of(inputSource)) + .outputSourceConfig(List.of(outputSource)) + .resolutionTechniques(type) + .build(); + + return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) + .whenComplete((response, exception) -> { + if (response != null) { + System.out.println("Workflow created successfully with ID: " + response.workflowArn()); + } else { + throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); + } + }) + .thenApply(CreateMatchingWorkflowResponse::workflowArn); + } + // snippet-end:[entityres.java2_create_matching_workflow.main] + + + /** + * Uploads a local file to an Amazon S3 bucket asynchronously. + * + * @param bucketName the name of the S3 bucket to upload the file to + * @param json the JSON data to be uploaded + * @return a {@link CompletableFuture} representing the asynchronous operation of uploading the file + * @throws RuntimeException if an error occurs during the file upload + */ + public CompletableFuture uploadLocalFileAsync(String bucketName, String json) { + + String key = "data/data.json"; // Corrected: No leading "/" + PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType("application/json") + .build(); + + CompletableFuture response = getS3AsyncClient().putObject(objectRequest, AsyncRequestBody.fromString(json)); + return response.whenComplete((resp, ex) -> { + if (ex != null) { + throw new RuntimeException("Failed to upload file", ex); + } + }); + } + + /** + * Checks if a specific object exists in an Amazon S3 bucket. + * + * @param bucketName the name of the S3 bucket to check + * @return true if the object exists, false otherwise + */ + public boolean doesObjectExist(String bucketName) { + try { + String key = "data/data.json"; + getS3AsyncClient().headObject(HeadObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build()); + return true; // File exists + + } catch (S3Exception e) { + return false; + } + } + +} diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java new file mode 100644 index 00000000000..9a6705150ae --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -0,0 +1,261 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity.scenario; + + +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.CompletionException; + +public class EntityResScenario { + public static final String DASHES = new String(new char[80]).replace("\0", "-"); + private static final String ROLES_STACK = "EntityResolutionCdkStack"; + public static void main (String[]args) throws InterruptedException { + + final String usage = """ + + Usage: + + + Where: + workflowName - A unique identifier for the matching workflow, used in the entity resolution process. + schemaName - The name of the schema, which defines the structure and attributes for the data being processed. + roleARN: The ARN of the IAM role, that grants permissions for the entity resolution workflow (this resource is created using the CDK script. See the Readme). + dataS3bucket: The S3 bucket,that stores the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. + outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. + inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. + """; + String workflowName = "MyMatchingWorkflow433"; + String schemaName = "schema232"; + + // Use the AWS CDK to create this AWS resources. See the Readme file. + String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; + String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; + String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + + EntityResActions actions = new EntityResActions(); + Scanner scanner = new Scanner(System.in); + System.out.println("Welcome to the AWS Entity Resolution Scenario. "); + System.out.println(""" + AWS Entity Resolution is a fully-managed machine learning service provided by + Amazon Web Services (AWS) that helps organizations extract, link, and + organize information from multiple data sources. It leverages natural + language processing and deep learning models to identify and resolve + entities, such as people, places, organizations, and products, + across structured and unstructured data. + + With Entity Resolution, customers can build robust data integration + pipelines to combine and reconcile data from multiple systems, databases, + and documents. The service can handle ambiguous, incomplete, or conflicting + information, and provide a unified view of entities and their relationships. + This can be particularly valuable in applications such as customer 360, + fraud detection, supply chain management, and knowledge management, where + accurate entity identification is crucial. + + The `EntityResolutionAsyncClient` interface in the AWS SDK for Java 2.x + provides a set of methods to programmatically interact with the AWS Entity + Resolution service. This allows developers to automate the entity extraction, + linking, and deduplication process as part of their data processing workflows. + With Entity Resolution, organizations can unlock the value of their data, + improve decision-making, and enhance customer experiences by having a reliable, + comprehensive view of their key entities. + """); + + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + + /* + This JSON is a valid input for the AWS Entity Resolution service. + The JSON represents an array of three objects, each containing an "id", "name", and "email" + property. This format aligns with the expected input structure for the + Entity Resolution service. + */ + String json = """ + [ + { + "id": "1", + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + { + "id": "2", + "name": "Bob Smith", + "email": "bob.smith@example.com" + }, + { + "id": "3", + "name": "Charlie Black", + "email": "charlie.black@example.com" + } + ] + """; + System.out.println("Upload the JSON to the "+ dataS3bucket +" S3 bucket if it does not exist"); + System.out.println(json); + waitForInputToContinue(scanner); + if (!actions.doesObjectExist(dataS3bucket)) { + actions.uploadLocalFileAsync(dataS3bucket, json); + } else { + System.out.println("The JSON exists in "+ dataS3bucket); + } + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("Create Schema Mapping"); + System.out.println(""" + Entity Resolution Schema Mapping aligns and integrates data from + multiple sources by identifying and matching corresponding entities + like customers or products. It unifies schemas, resolves conflicts, + and uses machine learning to link related entities, enabling a + consolidated, accurate view for improved data quality and decision-making. + + In this example, the schema mapping lines up with the fields in the JSON. That is, + it contains these fields: id, name, and email. + """); + waitForInputToContinue(scanner); + try { + actions.createSchemaMappingAsync(schemaName).join(); + System.out.println("Schema mapping was successfully created."); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("Create an AWS Entity Resolution Workflow. "); + System.out.println(""" + An Entity Resolution matching workflow identifies and links records + across datasets that represent the same real-world entity, such as + customers or products. Using techniques like schema mapping, + data profiling, and machine learning algorithms, + it evaluates attributes like names or emails to detect duplicates + or relationships, even with variations or inconsistencies. + The workflow outputs consolidated, de-duplicated data,\s + """); + waitForInputToContinue(scanner); + try { + String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + System.out.println("The workflow was successfully created. The ARN is: " + workflowArn); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + waitForInputToContinue(scanner); + + System.out.println(DASHES); + System.out.println("3. Start the matching job of the " +workflowName +" workflow."); + waitForInputToContinue(scanner); + String jobId = null; + try { + jobId = actions.startMatchingJobAsync(workflowName).join(); + System.out.println("The matching job was successfully started. Job ID: " + jobId); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("4. Get job details."); + waitForInputToContinue(scanner); + actions.getMatchingJobAsync(jobId, workflowName); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("5. Get Schema Mapping."); + waitForInputToContinue(scanner); + try { + actions.getSchemaMappingAsync(schemaName).join(); + System.out.println("Schema mapping retrieval completed."); + } catch (CompletionException ce) { + System.err.println("Error retrieving schema mapping: " + ce.getCause().getMessage()); + } + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("9. Delete the AWS Entity Resolution Workflow."); + System.out.println(""" + You cannot delete a workflow that is in a running state. + Would you like to wait for the workflow to complete. + This can take up to 30 mins (y/n). + """); + String delAns = scanner.nextLine().trim(); + if (delAns.equalsIgnoreCase("y")) { + System.out.println("You selected to delete Entity Resolution Workflow."); + waitForInputToContinue(scanner); + countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + try { + actions.deleteMatchingWorkflowAsync(workflowName).join(); + System.out.println("Workflow deleted successfully!"); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + // CloudFormationHelper.destroyCloudFormationStack(ROLES_STACK); + } + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("This concludes the AWS Entity Resolution scenario."); + System.out.println(DASHES); + + + } + + private static void waitForInputToContinue(Scanner scanner) { + while (true) { + System.out.println(""); + System.out.println("Enter 'c' followed by to continue:"); + String input = scanner.nextLine(); + + if (input.trim().equalsIgnoreCase("c")) { + System.out.println("Continuing with the program..."); + System.out.println(""); + break; + } else { + // Handle invalid input. + System.out.println("Invalid input. Please try again."); + } + } + } + + public static void countdownWithWorkflowCheck(EntityResActions actions, int totalSeconds, String jobId, String workflowName) throws InterruptedException { + int secondsElapsed = 0; + + while (true) { + // Calculate display minutes and seconds + int remainingTime = totalSeconds - secondsElapsed; + int displayMinutes = remainingTime / 60; + int displaySeconds = remainingTime % 60; + + // Print the countdown + System.out.printf("\r%02d:%02d", displayMinutes, displaySeconds); + Thread.sleep(1000); // Wait for 1 second + secondsElapsed++; + + // Check workflow status every 60 seconds + if (secondsElapsed % 60 == 0 || remainingTime <= 0) { + if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { + System.out.println(); // Move to the next line after countdown + System.out.println("Countdown complete: Workflow is in SUCCEEDED state!"); + break; + } + } + + // If countdown reaches zero, reset it for continuous countdown + if (remainingTime <= 0) { + secondsElapsed = 0; + } + } + } + +} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/glue.yaml b/javav2/example_code/entityresolution/src/main/resources/glue.yaml new file mode 100644 index 00000000000..d09227c86fe --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/glue.yaml @@ -0,0 +1,240 @@ +Resources: + GlueDataBucket278CFAC6: + Type: AWS::S3::Bucket + Properties: + BucketName: glue-2cf5649393c7465f926ae00d0592eba8 + VersioningConfiguration: + Status: Enabled + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + Metadata: + aws:cdk:path: EntityResolutionCdkStack/GlueDataBucket/Resource + GlueDatabase: + Type: AWS::Glue::Database + Properties: + CatalogId: + Ref: AWS::AccountId + DatabaseInput: + Name: entity_resolution_db + Metadata: + aws:cdk:path: EntityResolutionCdkStack/GlueDatabase + GlueTable: + Type: AWS::Glue::Table + Properties: + CatalogId: + Ref: AWS::AccountId + DatabaseName: + Ref: GlueDatabase + TableInput: + Name: entity_resolution + StorageDescriptor: + Columns: + - Name: id + Type: string + - Name: name + Type: string + - Name: email + Type: string + InputFormat: org.apache.hadoop.mapred.TextInputFormat + Location: + Fn::Join: + - "" + - - s3:// + - Ref: GlueDataBucket278CFAC6 + - /data/ + OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat + SerdeInfo: + Parameters: + serialization.format: "1" + SerializationLibrary: org.openx.data.jsonserde.JsonSerDe + TableType: EXTERNAL_TABLE + DependsOn: + - GlueDatabase + Metadata: + aws:cdk:path: EntityResolutionCdkStack/GlueTable + EntityResolutionRoleB51A51D3: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: entityresolution.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/AmazonS3FullAccess + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/AWSEntityResolutionConsoleFullAccess + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/AWSGlueConsoleFullAccess + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSGlueServiceRole + Metadata: + aws:cdk:path: EntityResolutionCdkStack/EntityResolutionRole/Resource + EntityResolutionRoleDefaultPolicy586C8066: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - entityresolution:GetMatchingWorkflow + - entityresolution:StartMatchingWorkflow + Effect: Allow + Resource: "*" + Version: "2012-10-17" + PolicyName: EntityResolutionRoleDefaultPolicy586C8066 + Roles: + - Ref: EntityResolutionRoleB51A51D3 + Metadata: + aws:cdk:path: EntityResolutionCdkStack/EntityResolutionRole/DefaultPolicy/Resource + OutputBucket7114EB27: + Type: AWS::S3::Bucket + Properties: + BucketName: entity-resolution-output-entityresolutioncdkstack + VersioningConfiguration: + Status: Enabled + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + Metadata: + aws:cdk:path: EntityResolutionCdkStack/OutputBucket/Resource + CDKMetadata: + Type: AWS::CDK::Metadata + Properties: + Analytics: v2:deflate64:H4sIAAAAAAAA/02MMQ+CMBSEfwt7eYLEuFsnFwy6m0cp5klpDW0lpul/N7SL0919d7k91M0BqgJXW4phKhX1EG4OxcRwtY9gGwgnLybpGB91dpE9lZcQ+KjP6LBHK7fyjr2SkRHOEDqjEkt6NYrEd4vZxcg6aY1fRNq03r19uv+n3OiBHBkd2QU/uKuPUEHdFC9LVC5eO5oldFl/L3LHkcUAAAA= + Metadata: + aws:cdk:path: EntityResolutionCdkStack/CDKMetadata/Default + Condition: CDKMetadataAvailable +Outputs: + EntityResolutionArn: + Description: The ARN of the Glue Role + Value: + Fn::GetAtt: + - EntityResolutionRoleB51A51D3 + - Arn + GlueTableArn: + Description: The ARN of the Glue Table + Value: + Fn::Join: + - "" + - - "arn:aws:glue:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :table/ + - Ref: GlueDatabase + - /entity_resolution + GlueDataBucketName: + Description: The name of the Glue Data Bucket + Value: + Ref: GlueDataBucket278CFAC6 +Conditions: + CDKMetadataAvailable: + Fn::Or: + - Fn::Or: + - Fn::Equals: + - Ref: AWS::Region + - af-south-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-east-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-northeast-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-northeast-2 + - Fn::Equals: + - Ref: AWS::Region + - ap-south-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-southeast-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-southeast-2 + - Fn::Equals: + - Ref: AWS::Region + - ca-central-1 + - Fn::Equals: + - Ref: AWS::Region + - cn-north-1 + - Fn::Equals: + - Ref: AWS::Region + - cn-northwest-1 + - Fn::Or: + - Fn::Equals: + - Ref: AWS::Region + - eu-central-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-north-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-south-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-west-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-west-2 + - Fn::Equals: + - Ref: AWS::Region + - eu-west-3 + - Fn::Equals: + - Ref: AWS::Region + - il-central-1 + - Fn::Equals: + - Ref: AWS::Region + - me-central-1 + - Fn::Equals: + - Ref: AWS::Region + - me-south-1 + - Fn::Equals: + - Ref: AWS::Region + - sa-east-1 + - Fn::Or: + - Fn::Equals: + - Ref: AWS::Region + - us-east-1 + - Fn::Equals: + - Ref: AWS::Region + - us-east-2 + - Fn::Equals: + - Ref: AWS::Region + - us-west-1 + - Fn::Equals: + - Ref: AWS::Region + - us-west-2 +Parameters: + BootstrapVersion: + Type: AWS::SSM::Parameter::Value + Default: /cdk-bootstrap/hnb659fds/version + Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] +Rules: + CheckBootstrapVersion: + Assertions: + - Assert: + Fn::Not: + - Fn::Contains: + - - "1" + - "2" + - "3" + - "4" + - "5" + - Ref: BootstrapVersion + AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI. + From eddbf02cc5cd682ea6b976137a46f1e190858cc2 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 09:52:50 -0500 Subject: [PATCH 002/144] add a new location for Basic Specs --- scenarios/basics/entity_resolution/README.md | 39 +++ .../basics/entity_resolution/SPECIFICATION.md | 328 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 scenarios/basics/entity_resolution/README.md create mode 100644 scenarios/basics/entity_resolution/SPECIFICATION.md diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md new file mode 100644 index 00000000000..9df7e209432 --- /dev/null +++ b/scenarios/basics/entity_resolution/README.md @@ -0,0 +1,39 @@ +## Overview +This AWS IoT SiteWise Service basic scenario demonstrates how to interact with the AWS IoT SiteWise service using an AWS SDK. The scenario covers various operations such as creating an Asset Model, creating assets, sending data to assets, and retrieving data. + +## Key Operations + +1. **Create an AWS SiteWise Asset Model**: + - This step creates an AWS SiteWise Asset Model by invoking the `createAssetModel` method. + +2. **Create an AWS IoT SiteWise Asset**: + - This operation creates an AWS SiteWise asset. + +3. **Retrieve the property ID values**: + - To send data to an asset, we need to get the property ID values for the model properties. This scenario uses temperature and humidity properties. + +4. **Send data to an AWS IoT SiteWise Asset**: + - This operation sends data to an IoT SiteWise Asset. + +5. **Retrieve the value of the IoT SiteWise Asset property**: + - This operation gets data from the asset. + +**Note** See the Eng spec for a full listing of operations. + +## Resources + +This Basics scenario requires an IAM role that has permissions to work with the AWS IoT SiteWise service. The scenario creates this resource using a CloudFormation template. + +## Implementations + +This scenario example will be implemented in the following languages: + +- Java +- Python +- JavaScript + +## Additional Reading + +- [AWS IoT SiteWise Documentation](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/what-is-sitewise.html) + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md new file mode 100644 index 00000000000..3b764c87468 --- /dev/null +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -0,0 +1,328 @@ +# AWS Entity Resolution Service Scenario Specification + +## Overview +This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution using an AWS SDK. It demonstrates various tasks such as creating a Schema Mapping, creating an matching workflow, starting the workflow, and so on. Finally this scenario demonstrates how to clean up resources. Its purpose is to demonstrate how to get up and running with AWS Entity Resolution and an AWS SDK. + +## Resources +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database and a table, and two S3 buckets. A CDK script is provided to create these resources. + +## Hello AWS Entity Resolution +This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listIdMappingJobsPaginator` to demonstrate how you can read through workflow job information. + +## Basics Scenario Program Flow +The AWS Entity Resolution Basics scenario executes the following operations. + +1. **Create a Schema Mapping**: + - Description: Creates a schema mapping invoking the `createSchemaMapping` method. + - Exception Handling: Check to see if a `ConflictException` is thrown. + If it is thrown, display the information and end the program. + +2. **Create a Matching Workflow**: + - Description: Creates a new matching workflow, defining how entities should be resolved and matched.. + - The method `createMatchingWorkflow` is called. + - Exception Handling: Check to see if a `ConflictException` is thrown if a conflict in the current state of the resource exists. If so, + display the message and end the program. + +3. **Start Matching Workflow**: + - Description: Initiates a matching workflow to process entity resolution based on predefined configurations. + - The method `listAssetModelProperties` is called to retrieve the property ID values. + - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. There are not + many other useful exceptions for this specific call. If so, display the message and end the program. + +4. **Send data to an AWS IoT SiteWise Asset**: + - Description: This operation sends data to an IoT SiteWise Asset. + - This step uses the method `batchPutAssetPropertyValue`. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +5. **Retrieve the value of the IoT SiteWise Asset property**: + - Description: This operation gets data from the asset. + - This step uses the method `getAssetPropertyValue`. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +6. **Create an IoT SiteWise Portal**: + - Description: This operation creates an IoT SiteWise portal. + - The method `createPortal` is called. + - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. + +7. **Describe the Portal**: + - Description: This operation describes the portal and returns a URL for the portal. + - The method `describePortal` is called and returns the URL. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +8. **Create an IoT SiteWise Gateway**: + - Description: This operation creates an IoT SiteWise Gateway. + - The method `createGateway` is called. + - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. + +9. **Describe the IoT SiteWise Gateway**: + - Description: This operation describes the Gateway. + - The method `describeGateway` is called. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +10. **Delete the AWS IoT SiteWise Assets**: + - The `delete` methods are called to clean up the resources. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program." + +### Program execution +The following shows the output of the AWS IoT SiteWise Basics scenario in the console. + +``` +AWS IoT SiteWise is a fully managed industrial software-as-a-service (SaaS) that +makes it easy to collect, store, organize, and monitor data from industrial equipment and processes. +It is designed to help industrial and manufacturing organizations collect data from their equipment and +processes, and use that data to make informed decisions about their operations. + +One of the key features of AWS IoT SiteWise is its ability to connect to a wide range of industrial +equipment and systems, including programmable logic controllers (PLCs), sensors, and other +industrial devices. It can collect data from these devices and organize it into a unified data model, +making it easier to analyze and gain insights from the data. AWS IoT SiteWise also provides tools for +visualizing the data, setting up alarms and alerts, and generating reports. + +Another key feature of AWS IoT SiteWise is its ability to scale to handle large volumes of data. +It can collect and store data from thousands of devices and process millions of data points per second, +making it suitable for large-scale industrial operations. Additionally, AWS IoT SiteWise is designed +to be secure and compliant, with features like role-based access controls, data encryption, +and integration with other AWS services for additional security and compliance features. + +Let's get started... + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +Use AWS CloudFormation to create an IAM role that are required for this scenario. +Stack creation requested, ARN is arn:aws:cloudformation:us-east-1:814548047983:stack/RoleSitewise/29f480c0-75fd-11ef-a42e-12cd4e534049 +Stack created successfully +-------------------------------------------------------------------------------- +1. Create an AWS SiteWise Asset Model + An AWS IoT SiteWise Asset Model is a way to represent the physical assets, such as equipment, + processes, and systems, that exist in an industrial environment. This model provides a structured and + hierarchical representation of these assets, allowing users to define the relationships and properties + of each asset. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The Asset Model MyAssetModel already exists. The id of the existing model is ffbc475b-73ad-4eb6-bf28-8728818fa8ef. Moving on... + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +2. Create an AWS IoT SiteWise Asset + The IoT SiteWise model defines the structure and metadata for your physical assets. Now we + can use the asset model to create the asset. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Asset created with ID: 4d681624-a303-46dd-8830-6189790ae915 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +3. Retrieve the property ID values + To send data to an asset, we need to get the property ID values for the + Temperature and Humidity properties. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The Humidity property Id is feb4aba6-55f9-4b00-b366-27b9d7e5a747 +The Temperature property Id is 6cb505aa-6bcc-46f4-a12a-7ca5df8eb028 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +4. Send data to an AWS IoT SiteWise Asset +By sending data to an IoT SiteWise Asset, you can aggregate data from +multiple sources, normalize the data into a standard format, and store it in a +centralized location. This makes it easier to analyze and gain insights from the data. + +This example demonstrate how to generate sample data and ingest it into the AWS IoT SiteWise asset. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Data sent successfully. + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +5. Retrieve the value of the IoT SiteWise Asset property +IoT SiteWise is an AWS service that allows you to collect, process, and analyze industrial data +from connected equipment and sensors. One of the key benefits of reading an IoT SiteWise property +is the ability to gain valuable insights from your industrial data. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The property name is: Temperature property +The value of this property is 23.5 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The property name is: Humidity property +The value of this property is 65.0 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +6. Create an IoT SiteWise Portal + An IoT SiteWise Portal allows you to aggregate data from multiple industrial sources, + such as sensors, equipment, and control systems, into a centralized platform. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Portal created successfully. Portal ID 63e65729-b7a1-410a-aa36-94145fe92153 +The portal Id is 63e65729-b7a1-410a-aa36-94145fe92153 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +7. Describe the Portal + In this step, we will describe the step and provide the portal URL. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Portal URL: https://p-fy9qnrqy.app.iotsitewise.aws + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +8. Create an IoTSitewise Gateway +IoTSitewise Gateway serves as the bridge between industrial equipment, sensors, and the +cloud-based IoTSitewise service. It is responsible for securely collecting, processing, and +transmitting data from various industrial assets to the IoTSitewise platform, +enabling real-time monitoring, analysis, and optimization of industrial operations. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The ARN of the gateway is arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 +Gateway creation completed successfully. id is 50320670-1d88-4a7e-9013-1d7e8a3af832 +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +9. Describe the IoTSitewise Gateway + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Gateway Name: myGateway11 +Gateway ARN: arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 +Gateway Platform: GatewayPlatform(GreengrassV2=GreengrassV2(CoreDeviceThingName=myThing78)) +Gateway Creation Date: 2024-09-18T20:34:13.117Z +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +10. Delete the AWS IoT SiteWise Assets +Before you can delete the Asset Model, you must delete the assets. + + +Would you like to delete the IoT Sitewise Assets? (y/n) +y +You selected to delete the Sitewise assets. + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Portal 63e65729-b7a1-410a-aa36-94145fe92153 was deleted successfully. +An unexpected error occurred: Cannot invoke "java.util.concurrent.CompletableFuture.join()" because "future" is null +Asset deleted successfully. +Lets wait 1 min for the asset to be deleted +01:00The Gateway was deleted successfully +00:00Countdown complete! + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Delete the AWS IoT SiteWise Asset Model +Asset model deleted successfully. + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +Delete stack requested .... +Stack deleted successfully. +This concludes the AWS SiteWise Scenario +-------------------------------------------------------------------------------- + + +``` + +## SOS Tags + +The following table describes the metadata used in this Basics Scenario. + + +| action | metadata file | metadata key | +|--------------------------------|-----------------------------------|---------------------------------------- | +| `describeGateway` | iot_sitewise_metadata.yaml | iotsitewise_DescribeGateway | +| `deleteGateway ` | iot_sitewise_metadata.yaml | iotsitewise_DeleteGateway | +| `createGateway ` | iot_sitewise_metadata.yaml | iotsitewise_CreateGateway | +| `describePortal` | iot_sitewise_metadata.yaml | iotsitewise_DescribePortal | +| `listAssetModels` | iot_sitewise_metadata.yaml | iotsitewise_ListAssetModels | +| `deletePortal` | iot_sitewise_metadata.yaml | iotsitewise_DeletePortal | +| `createPortal` | iot_sitewise_metadata.yaml | iotsitewise_CreatePortal | +| `deleteAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAssetModel | +| `deleteAsset` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAsset | +| `describeAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DescribeAssetModel | +| `getAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_GetAssetPropertyValue | +| `batchPutAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_BatchPutAssetPropertyValue | +| `createAsset` | iot_sitewise_metadata.yaml | iotsitewise_CreateAsset | +| `createAssetModel ` | iot_sitewise_metadata.yaml | iotsitewise_CreateAssetModel | +| `scenario` | iot_sitewise_metadata.yaml | iotsitewise_Scenario | +| `hello` | iot_sitewise_metadata.yaml | iotsitewise_Hello | + + + From d7a507448fdfcf712ca3c72cecab9daffad0082f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 08:59:18 -0600 Subject: [PATCH 003/144] Bump aws-cdk-lib from 2.132.1 to 2.177.0 in /resources/cdk/aurora_serverless_app (#7209) Bump aws-cdk-lib in /resources/cdk/aurora_serverless_app Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk-lib) from 2.132.1 to 2.177.0. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.177.0/packages/aws-cdk-lib) --- updated-dependencies: - dependency-name: aws-cdk-lib dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../aurora_serverless_app/package-lock.json | 136 ++++++++++-------- .../cdk/aurora_serverless_app/package.json | 4 +- 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/resources/cdk/aurora_serverless_app/package-lock.json b/resources/cdk/aurora_serverless_app/package-lock.json index ac787aebbf7..1431a0813a1 100644 --- a/resources/cdk/aurora_serverless_app/package-lock.json +++ b/resources/cdk/aurora_serverless_app/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "aws-cdk": "^2.115.0", - "aws-cdk-lib": "^2.132.1", "constructs": "^10.0.38", "source-map-support": "^0.5.21" }, @@ -19,7 +18,7 @@ "devDependencies": { "@types/jest": "^27.4.0", "@types/node": "^17.0.10", - "aws-cdk-lib": "^2.132.1", + "aws-cdk-lib": "^2.177.0", "constructs": "^10.0.38", "jest": "^27.4.7", "ts-jest": "^27.1.3", @@ -45,22 +44,61 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.202", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", - "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==", - "dev": true + "version": "2.2.221", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.221.tgz", + "integrity": "sha512-+Vu2cMvgtkaHwNezrTVng4+FAMAWKJTkC/2ZQlgkbY05k0lHHK/2eWKqBhTeA7EpxVrx9uFN7GdBFz3mcThpxg==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-kubectl-v20": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", - "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", + "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", - "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "39.2.8", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.8.tgz", + "integrity": "sha512-VkppFgLbm5M1/K8S+BimI/0COq+E9fCDcdDyAe4gFizHNZTALZA4sMds2kug1rtPFKCcWAexrycs2D4iQHcRCw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.6.3" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.6.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@babel/code-frame": { "version": "7.23.5", @@ -1328,9 +1366,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.132.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.132.1.tgz", - "integrity": "sha512-VheC7WcvmxiteNaZPucS9J9haGQZwbUtwNiNqsbTaEiru6ETUhf/yIOIamLto1kOKEPxCw2bfLkgYrWoCzwOpw==", + "version": "2.177.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.177.0.tgz", + "integrity": "sha512-nTnHAwjZaPJ5gfJjtzE/MyK6q0a66nWthoJl7l8srucRb+I30dczhbbXor6QCdVpJaTRAEliMOMq23aglsAQbg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -1345,20 +1383,22 @@ "mime-types" ], "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.202", - "@aws-cdk/asset-kubectl-v20": "^2.1.2", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-kubectl-v20": "^2.1.3", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^39.2.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", - "ignore": "^5.3.1", + "ignore": "^5.3.2", "jsonschema": "^1.4.1", "mime-types": "^2.1.35", "minimatch": "^3.1.2", "punycode": "^2.3.1", - "semver": "^7.6.0", - "table": "^6.8.1", + "semver": "^7.6.3", + "table": "^6.8.2", "yaml": "1.10.2" }, "engines": { @@ -1375,15 +1415,15 @@ "license": "Apache-2.0" }, "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.12.0", + "version": "8.17.1", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -1484,6 +1524,12 @@ "inBundle": true, "license": "MIT" }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" + }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { "version": "11.2.0", "dev": true, @@ -1505,7 +1551,7 @@ "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.3.1", + "version": "5.3.2", "dev": true, "inBundle": true, "license": "MIT", @@ -1555,18 +1601,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/aws-cdk-lib/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/aws-cdk-lib/node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -1619,13 +1653,10 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.6.0", + "version": "7.6.3", "dev": true, "inBundle": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -1677,7 +1708,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.1", + "version": "6.8.2", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -1701,21 +1732,6 @@ "node": ">= 10.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, "node_modules/aws-cdk-lib/node_modules/yaml": { "version": "1.10.2", "dev": true, diff --git a/resources/cdk/aurora_serverless_app/package.json b/resources/cdk/aurora_serverless_app/package.json index 6cfc9c08894..b33c40d89c0 100644 --- a/resources/cdk/aurora_serverless_app/package.json +++ b/resources/cdk/aurora_serverless_app/package.json @@ -17,7 +17,7 @@ "devDependencies": { "@types/jest": "^27.4.0", "@types/node": "^17.0.10", - "aws-cdk-lib": "^2.132.1", + "aws-cdk-lib": "^2.177.0", "constructs": "^10.0.38", "jest": "^27.4.7", "ts-jest": "^27.1.3", @@ -29,6 +29,6 @@ "aws-cdk-lib": "^2.80.0", "constructs": "^10.0.38", "source-map-support": "^0.5.21", - "aws-cdk-lib": "^2.132.1" + "aws-cdk-lib": "^2.177.0" } } From 1c67212ab3bba78e95414f576ad7d7ad0784f376 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:21:22 -0600 Subject: [PATCH 004/144] Bump aws-cdk-lib from 2.85.0 to 2.177.0 in /applications/feedback_sentiment_analyzer/cdk (#7213) Bump aws-cdk-lib in /applications/feedback_sentiment_analyzer/cdk Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk-lib) from 2.85.0 to 2.177.0. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.177.0/packages/aws-cdk-lib) --- updated-dependencies: - dependency-name: aws-cdk-lib dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../cdk/package-lock.json | 260 +++++++++++------- .../cdk/package.json | 2 +- 2 files changed, 157 insertions(+), 105 deletions(-) diff --git a/applications/feedback_sentiment_analyzer/cdk/package-lock.json b/applications/feedback_sentiment_analyzer/cdk/package-lock.json index e5b2152fdde..993b74645e1 100644 --- a/applications/feedback_sentiment_analyzer/cdk/package-lock.json +++ b/applications/feedback_sentiment_analyzer/cdk/package-lock.json @@ -8,7 +8,7 @@ "name": "cdk", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "^2.85.0", + "aws-cdk-lib": "^2.177.0", "constructs": "^10.2.60", "source-map-support": "^0.5.21" }, @@ -41,19 +41,55 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.186", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.186.tgz", - "integrity": "sha512-2wSuOWQlrWc0AFuPCzXYn2Y8oK2vTfpNrVa8dxBxfswbwUrXMAirhpsP1f1J/4KEhA/4Hs4l27dKiC/IcDrvIQ==" + "version": "2.2.221", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.221.tgz", + "integrity": "sha512-+Vu2cMvgtkaHwNezrTVng4+FAMAWKJTkC/2ZQlgkbY05k0lHHK/2eWKqBhTeA7EpxVrx9uFN7GdBFz3mcThpxg==", + "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-kubectl-v20": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.1.tgz", - "integrity": "sha512-U1ntiX8XiMRRRH5J1IdC+1t5CE89015cwyt5U63Cpk0GnMlN5+h9WsWMlKlPXZR4rdq/m806JRlBMRpBUB2Dhw==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", + "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "39.2.9", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.9.tgz", + "integrity": "sha512-Ao4C8WoM5wgU4yn0aKLvI4gtgiRDa+8bVVwOlhGK9/jHmZlgMZY44UY9muq6qMKsMXTmfQeaB8LS3JLOiEUheA==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.0" + } }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v5": { - "version": "2.0.155", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.155.tgz", - "integrity": "sha512-Q+Ny25hUPINlBbS6lmbUr4m6Tr6ToEJBla7sXA3FO3JUD0Z69ddcgbhuEBF8Rh1a2xmPONm89eX77kwK2fb4vQ==" + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.0", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", @@ -2439,9 +2475,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.85.0.tgz", - "integrity": "sha512-u+ypK8XEMRH3tGRMSmcbPYxLet7xBdGIztUkMcPtlNJGhS/vxqh12yYkem3g3zzmHwdX8OPLSnlZ2sIuiIqp/g==", + "version": "2.177.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.177.0.tgz", + "integrity": "sha512-nTnHAwjZaPJ5gfJjtzE/MyK6q0a66nWthoJl7l8srucRb+I30dczhbbXor6QCdVpJaTRAEliMOMq23aglsAQbg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2452,21 +2488,25 @@ "punycode", "semver", "table", - "yaml" + "yaml", + "mime-types" ], + "license": "Apache-2.0", "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.177", - "@aws-cdk/asset-kubectl-v20": "^2.1.1", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-kubectl-v20": "^2.1.3", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^39.2.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.1.1", - "ignore": "^5.2.4", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", "minimatch": "^3.1.2", - "punycode": "^2.3.0", - "semver": "^7.5.1", - "table": "^6.8.1", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", "yaml": "1.10.2" }, "engines": { @@ -2482,14 +2522,14 @@ "license": "Apache-2.0" }, "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.12.0", + "version": "8.17.1", "inBundle": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2579,8 +2619,13 @@ "inBundle": true, "license": "MIT" }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.1.1", + "version": "11.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -2598,7 +2643,7 @@ "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.2.4", + "version": "5.3.2", "inBundle": true, "license": "MIT", "engines": { @@ -2642,15 +2687,23 @@ "inBundle": true, "license": "MIT" }, - "node_modules/aws-cdk-lib/node_modules/lru-cache": { - "version": "6.0.0", + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", "inBundle": true, - "license": "ISC", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, "node_modules/aws-cdk-lib/node_modules/minimatch": { @@ -2665,7 +2718,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.0", + "version": "2.3.1", "inBundle": true, "license": "MIT", "engines": { @@ -2681,12 +2734,9 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.5.2", + "version": "7.6.3", "inBundle": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -2735,7 +2785,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.1", + "version": "6.8.2", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -2750,26 +2800,13 @@ } }, "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", "inBundle": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/uri-js": { - "version": "4.4.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, "node_modules/aws-cdk-lib/node_modules/yaml": { "version": "1.10.2", "inBundle": true, @@ -5359,19 +5396,38 @@ } }, "@aws-cdk/asset-awscli-v1": { - "version": "2.2.186", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.186.tgz", - "integrity": "sha512-2wSuOWQlrWc0AFuPCzXYn2Y8oK2vTfpNrVa8dxBxfswbwUrXMAirhpsP1f1J/4KEhA/4Hs4l27dKiC/IcDrvIQ==" + "version": "2.2.221", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.221.tgz", + "integrity": "sha512-+Vu2cMvgtkaHwNezrTVng4+FAMAWKJTkC/2ZQlgkbY05k0lHHK/2eWKqBhTeA7EpxVrx9uFN7GdBFz3mcThpxg==" }, "@aws-cdk/asset-kubectl-v20": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.1.tgz", - "integrity": "sha512-U1ntiX8XiMRRRH5J1IdC+1t5CE89015cwyt5U63Cpk0GnMlN5+h9WsWMlKlPXZR4rdq/m806JRlBMRpBUB2Dhw==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", + "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==" }, - "@aws-cdk/asset-node-proxy-agent-v5": { - "version": "2.0.155", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.155.tgz", - "integrity": "sha512-Q+Ny25hUPINlBbS6lmbUr4m6Tr6ToEJBla7sXA3FO3JUD0Z69ddcgbhuEBF8Rh1a2xmPONm89eX77kwK2fb4vQ==" + "@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" + }, + "@aws-cdk/cloud-assembly-schema": { + "version": "39.2.9", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.9.tgz", + "integrity": "sha512-Ao4C8WoM5wgU4yn0aKLvI4gtgiRDa+8bVVwOlhGK9/jHmZlgMZY44UY9muq6qMKsMXTmfQeaB8LS3JLOiEUheA==", + "requires": { + "jsonschema": "~1.4.1", + "semver": "^7.7.0" + }, + "dependencies": { + "jsonschema": { + "version": "1.4.1", + "bundled": true + }, + "semver": { + "version": "7.7.0", + "bundled": true + } + } }, "@aws-crypto/crc32": { "version": "3.0.0", @@ -7343,22 +7399,24 @@ } }, "aws-cdk-lib": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.85.0.tgz", - "integrity": "sha512-u+ypK8XEMRH3tGRMSmcbPYxLet7xBdGIztUkMcPtlNJGhS/vxqh12yYkem3g3zzmHwdX8OPLSnlZ2sIuiIqp/g==", - "requires": { - "@aws-cdk/asset-awscli-v1": "^2.2.177", - "@aws-cdk/asset-kubectl-v20": "^2.1.1", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", + "version": "2.177.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.177.0.tgz", + "integrity": "sha512-nTnHAwjZaPJ5gfJjtzE/MyK6q0a66nWthoJl7l8srucRb+I30dczhbbXor6QCdVpJaTRAEliMOMq23aglsAQbg==", + "requires": { + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-kubectl-v20": "^2.1.3", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^39.2.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.1.1", - "ignore": "^5.2.4", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", "minimatch": "^3.1.2", - "punycode": "^2.3.0", - "semver": "^7.5.1", - "table": "^6.8.1", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", "yaml": "1.10.2" }, "dependencies": { @@ -7367,13 +7425,13 @@ "bundled": true }, "ajv": { - "version": "8.12.0", + "version": "8.17.1", "bundled": true, "requires": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" } }, "ansi-regex": { @@ -7430,8 +7488,12 @@ "version": "3.1.3", "bundled": true }, + "fast-uri": { + "version": "3.0.3", + "bundled": true + }, "fs-extra": { - "version": "11.1.1", + "version": "11.2.0", "bundled": true, "requires": { "graceful-fs": "^4.2.0", @@ -7444,7 +7506,7 @@ "bundled": true }, "ignore": { - "version": "5.2.4", + "version": "5.3.2", "bundled": true }, "is-fullwidth-code-point": { @@ -7471,11 +7533,15 @@ "version": "4.4.2", "bundled": true }, - "lru-cache": { - "version": "6.0.0", + "mime-db": { + "version": "1.52.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.35", "bundled": true, "requires": { - "yallist": "^4.0.0" + "mime-db": "1.52.0" } }, "minimatch": { @@ -7486,7 +7552,7 @@ } }, "punycode": { - "version": "2.3.0", + "version": "2.3.1", "bundled": true }, "require-from-string": { @@ -7494,11 +7560,8 @@ "bundled": true }, "semver": { - "version": "7.5.2", - "bundled": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "bundled": true }, "slice-ansi": { "version": "4.0.0", @@ -7526,7 +7589,7 @@ } }, "table": { - "version": "6.8.1", + "version": "6.8.2", "bundled": true, "requires": { "ajv": "^8.0.1", @@ -7537,18 +7600,7 @@ } }, "universalify": { - "version": "2.0.0", - "bundled": true - }, - "uri-js": { - "version": "4.4.1", - "bundled": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "yallist": { - "version": "4.0.0", + "version": "2.0.1", "bundled": true }, "yaml": { diff --git a/applications/feedback_sentiment_analyzer/cdk/package.json b/applications/feedback_sentiment_analyzer/cdk/package.json index 62213a5fe64..0da93c5cb1d 100644 --- a/applications/feedback_sentiment_analyzer/cdk/package.json +++ b/applications/feedback_sentiment_analyzer/cdk/package.json @@ -22,7 +22,7 @@ "typescript": "~5.0.4" }, "dependencies": { - "aws-cdk-lib": "^2.85.0", + "aws-cdk-lib": "^2.177.0", "constructs": "^10.2.60", "source-map-support": "^0.5.21" } From daf4ac3ea32e6f47c6870d7de54cd6b478fb3185 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 10:56:21 -0500 Subject: [PATCH 005/144] updated the Basic Specs --- .../basics/entity_resolution/SPECIFICATION.md | 92 ++++++++----------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 3b764c87468..881d0df2e32 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -25,46 +25,38 @@ The AWS Entity Resolution Basics scenario executes the following operations. 3. **Start Matching Workflow**: - Description: Initiates a matching workflow to process entity resolution based on predefined configurations. - - The method `listAssetModelProperties` is called to retrieve the property ID values. - - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. There are not - many other useful exceptions for this specific call. If so, display the message and end the program. - -4. **Send data to an AWS IoT SiteWise Asset**: - - Description: This operation sends data to an IoT SiteWise Asset. - - This step uses the method `batchPutAssetPropertyValue`. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. - -5. **Retrieve the value of the IoT SiteWise Asset property**: - - Description: This operation gets data from the asset. - - This step uses the method `getAssetPropertyValue`. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. - -6. **Create an IoT SiteWise Portal**: - - Description: This operation creates an IoT SiteWise portal. - - The method `createPortal` is called. - - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. + - The method `startMatchingJob` is called to start the matching workflow. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + +4. **Get Workflow Job Details**: + - Description: Retrieves details about a specific matching workflow job. + - This step uses the method `getMatchingJob`. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + -7. **Describe the Portal**: - - Description: This operation describes the portal and returns a URL for the portal. - - The method `describePortal` is called and returns the URL. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. +5. **List Matching Workflows**: + - Description: Lists all matching workflows created within the account. + - This step uses the method `listMatchingWorkflows`. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. -8. **Create an IoT SiteWise Gateway**: - - Description: This operation creates an IoT SiteWise Gateway. - - The method `createGateway` is called. - - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. +6. **Get Schema Mapping**: + - Description: Lists all schema mappings available in the account. + - The method `createPortal` is called. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program + +7. **Tag Resource**: + - Description: Adds tags associated with an AWS Entity Resolution resource. + - The method `tagResource` is called. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program -9. **Describe the IoT SiteWise Gateway**: - - Description: This operation describes the Gateway. - - The method `describeGateway` is called. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. +8. **Delete Matching Workflow**: + - Description: Deletes a specified matching workflowy. + - The methods `deleteMatchingWorkflow` is called. + - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program -10. **Delete the AWS IoT SiteWise Assets**: - - The `delete` methods are called to clean up the resources. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program." ### Program execution -The following shows the output of the AWS IoT SiteWise Basics scenario in the console. +The following shows the output of the AWS Entity Resolution Basics scenario in the console. ``` AWS IoT SiteWise is a fully managed industrial software-as-a-service (SaaS) that @@ -303,26 +295,20 @@ This concludes the AWS SiteWise Scenario ## SOS Tags The following table describes the metadata used in this Basics Scenario. +| action | metadata file | metadata key | +|-------------------------|------------------------|---------------------------------------- | +| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | +| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | +| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | +| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | +| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | +| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | +| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | +| `tagResource ` | entity_metadata.yaml | entity_TagResource | +| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | +| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | +| `scenario` | entity_metadata.yaml | entity_Scenario | -| action | metadata file | metadata key | -|--------------------------------|-----------------------------------|---------------------------------------- | -| `describeGateway` | iot_sitewise_metadata.yaml | iotsitewise_DescribeGateway | -| `deleteGateway ` | iot_sitewise_metadata.yaml | iotsitewise_DeleteGateway | -| `createGateway ` | iot_sitewise_metadata.yaml | iotsitewise_CreateGateway | -| `describePortal` | iot_sitewise_metadata.yaml | iotsitewise_DescribePortal | -| `listAssetModels` | iot_sitewise_metadata.yaml | iotsitewise_ListAssetModels | -| `deletePortal` | iot_sitewise_metadata.yaml | iotsitewise_DeletePortal | -| `createPortal` | iot_sitewise_metadata.yaml | iotsitewise_CreatePortal | -| `deleteAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAssetModel | -| `deleteAsset` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAsset | -| `describeAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DescribeAssetModel | -| `getAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_GetAssetPropertyValue | -| `batchPutAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_BatchPutAssetPropertyValue | -| `createAsset` | iot_sitewise_metadata.yaml | iotsitewise_CreateAsset | -| `createAssetModel ` | iot_sitewise_metadata.yaml | iotsitewise_CreateAssetModel | -| `scenario` | iot_sitewise_metadata.yaml | iotsitewise_Scenario | -| `hello` | iot_sitewise_metadata.yaml | iotsitewise_Hello | - From c4de0782aba69a6e96384f6d00ddc4a3a36dc542 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 13:52:20 -0500 Subject: [PATCH 006/144] updated the Basic Specs --- .../entity/scenario/EntityResActions.java | 39 ++- .../entity/scenario/EntityResScenario.java | 63 +++-- .../basics/entity_resolution/SPECIFICATION.md | 232 +++++++----------- 3 files changed, 162 insertions(+), 172 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index b2dfda3e4f6..260a3bb482b 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -17,36 +17,28 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; -import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; -import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; import software.amazon.awssdk.services.entityresolution.model.OutputSource; -import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; -import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobResponse; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; -import software.amazon.awssdk.services.s3.model.NoSuchKeyException; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; -import java.nio.file.Paths; import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; public class EntityResActions { @@ -318,7 +310,7 @@ public CompletableFuture createMatchingWorkflowAsync(String roleARN, Str return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) .whenComplete((response, exception) -> { if (response != null) { - System.out.println("Workflow created successfully with ID: " + response.workflowArn()); + System.out.println("Workflow created successfully."); } else { throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); } @@ -327,6 +319,30 @@ public CompletableFuture createMatchingWorkflowAsync(String roleARN, Str } // snippet-end:[entityres.java2_create_matching_workflow.main] + // snippet-start:[entityres.java2_tag_resource.main] + /** + * Tags the specified schema mapping ARN. + * + * @param schemaMappingARN the ARN of the schema mapping to tag + */ + public CompletableFuture tagEntityResource(String schemaMappingARN) { + Map tags = new HashMap<>(); + tags.put("tag1", "tag1Value"); + tags.put("tag2", "tag2Value"); + + TagResourceRequest request = TagResourceRequest.builder() + .resourceArn(schemaMappingARN) + .tags(tags) + .build(); + + return getResolutionAsyncClient().tagResource(request) + .thenAccept(response -> System.out.println("Successfully tagged the resource.")) + .exceptionally(exception -> { + throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); + }); + } + + // snippet-end:[entityres.java2_tag_resource.main] /** * Uploads a local file to an Amazon S3 bucket asynchronously. @@ -373,4 +389,5 @@ public boolean doesObjectExist(String bucketName) { } } + } diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 9a6705150ae..83116dc1517 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -4,6 +4,8 @@ package com.example.entity.scenario; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; + import java.util.Map; import java.util.Scanner; import java.util.concurrent.CompletionException; @@ -11,7 +13,8 @@ public class EntityResScenario { public static final String DASHES = new String(new char[80]).replace("\0", "-"); private static final String ROLES_STACK = "EntityResolutionCdkStack"; - public static void main (String[]args) throws InterruptedException { + + public static void main(String[] args) throws InterruptedException { final String usage = """ @@ -26,13 +29,13 @@ public static void main (String[]args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = "MyMatchingWorkflow433"; - String schemaName = "schema232"; + String workflowName = "MyMatchingWorkflow450"; + String schemaName = "schema450"; // Use the AWS CDK to create this AWS resources. See the Readme file. String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; - String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; + String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; EntityResActions actions = new EntityResActions(); @@ -93,33 +96,34 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } ] """; - System.out.println("Upload the JSON to the "+ dataS3bucket +" S3 bucket if it does not exist"); + System.out.println("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); System.out.println(json); waitForInputToContinue(scanner); if (!actions.doesObjectExist(dataS3bucket)) { actions.uploadLocalFileAsync(dataS3bucket, json); } else { - System.out.println("The JSON exists in "+ dataS3bucket); + System.out.println("The JSON exists in " + dataS3bucket); } waitForInputToContinue(scanner); System.out.println(DASHES); System.out.println(DASHES); - System.out.println("Create Schema Mapping"); + System.out.println("1. Create Schema Mapping"); System.out.println(""" Entity Resolution Schema Mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - + In this example, the schema mapping lines up with the fields in the JSON. That is, it contains these fields: id, name, and email. """); waitForInputToContinue(scanner); + String mappingARN = null; try { - actions.createSchemaMappingAsync(schemaName).join(); - System.out.println("Schema mapping was successfully created."); + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); + mappingARN = response.schemaArn(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); System.err.println("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -129,7 +133,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println(DASHES); - System.out.println("Create an AWS Entity Resolution Workflow. "); + System.out.println("2. Create an AWS Entity Resolution Workflow. "); System.out.println(""" An Entity Resolution matching workflow identifies and links records across datasets that represent the same real-world entity, such as @@ -142,7 +146,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); try { String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); - System.out.println("The workflow was successfully created. The ARN is: " + workflowArn); + System.out.println("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); System.err.println("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -150,12 +154,12 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); System.out.println(DASHES); - System.out.println("3. Start the matching job of the " +workflowName +" workflow."); + System.out.println("3. Start the matching job of the " + workflowName + " workflow."); waitForInputToContinue(scanner); - String jobId = null; + String jobId = null; try { jobId = actions.startMatchingJobAsync(workflowName).join(); - System.out.println("The matching job was successfully started. Job ID: " + jobId); + System.out.println("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -164,9 +168,14 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println(DASHES); - System.out.println("4. Get job details."); + System.out.println("4. Get details for job "+jobId); waitForInputToContinue(scanner); - actions.getMatchingJobAsync(jobId, workflowName); + try { + actions.getMatchingJobAsync(jobId, workflowName).join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } System.out.println(DASHES); System.out.println(DASHES); @@ -181,7 +190,24 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println(DASHES); - System.out.println("9. Delete the AWS Entity Resolution Workflow."); + System.out.println("6. List Schema Mappings."); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("7. Tag the "+schemaName +"resource."); + System.out.println(""" + Tags can help you organize and categorize your Entity Resolution resources. + You can also use them to scope user permissions by granting a user permission + to access or change only resources with certain tag values. + In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, + the SchemaMapping is tagged. + """); + actions.tagEntityResource(mappingARN).join(); + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("8. Delete the AWS Entity Resolution Workflow."); System.out.println(""" You cannot delete a workflow that is in a running state. Would you like to wait for the workflow to complete. @@ -199,7 +225,6 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Throwable cause = ce.getCause(); System.err.println("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } - // CloudFormationHelper.destroyCloudFormationStack(ROLES_STACK); } waitForInputToContinue(scanner); System.out.println(DASHES); diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 881d0df2e32..0802942e9fd 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -59,24 +59,29 @@ The AWS Entity Resolution Basics scenario executes the following operations. The following shows the output of the AWS Entity Resolution Basics scenario in the console. ``` -AWS IoT SiteWise is a fully managed industrial software-as-a-service (SaaS) that -makes it easy to collect, store, organize, and monitor data from industrial equipment and processes. -It is designed to help industrial and manufacturing organizations collect data from their equipment and -processes, and use that data to make informed decisions about their operations. - -One of the key features of AWS IoT SiteWise is its ability to connect to a wide range of industrial -equipment and systems, including programmable logic controllers (PLCs), sensors, and other -industrial devices. It can collect data from these devices and organize it into a unified data model, -making it easier to analyze and gain insights from the data. AWS IoT SiteWise also provides tools for -visualizing the data, setting up alarms and alerts, and generating reports. - -Another key feature of AWS IoT SiteWise is its ability to scale to handle large volumes of data. -It can collect and store data from thousands of devices and process millions of data points per second, -making it suitable for large-scale industrial operations. Additionally, AWS IoT SiteWise is designed -to be secure and compliant, with features like role-based access controls, data encryption, -and integration with other AWS services for additional security and compliance features. - -Let's get started... +Welcome to the AWS Entity Resolution Scenario. +AWS Entity Resolution is a fully-managed machine learning service provided by +Amazon Web Services (AWS) that helps organizations extract, link, and +organize information from multiple data sources. It leverages natural +language processing and deep learning models to identify and resolve +entities, such as people, places, organizations, and products, +across structured and unstructured data. + +With Entity Resolution, customers can build robust data integration +pipelines to combine and reconcile data from multiple systems, databases, +and documents. The service can handle ambiguous, incomplete, or conflicting +information, and provide a unified view of entities and their relationships. +This can be particularly valuable in applications such as customer 360, +fraud detection, supply chain management, and knowledge management, where +accurate entity identification is crucial. + +The `EntityResolutionAsyncClient` interface in the AWS SDK for Java 2.x +provides a set of methods to programmatically interact with the AWS Entity +Resolution service. This allows developers to automate the entity extraction, +linking, and deduplication process as part of their data processing workflows. +With Entity Resolution, organizations can unlock the value of their data, +improve decision-making, and enhance customer experiences by having a reliable, +comprehensive view of their key entities. Enter 'c' followed by to continue: @@ -84,39 +89,35 @@ c Continuing with the program... -------------------------------------------------------------------------------- -Use AWS CloudFormation to create an IAM role that are required for this scenario. -Stack creation requested, ARN is arn:aws:cloudformation:us-east-1:814548047983:stack/RoleSitewise/29f480c0-75fd-11ef-a42e-12cd4e534049 -Stack created successfully --------------------------------------------------------------------------------- -1. Create an AWS SiteWise Asset Model - An AWS IoT SiteWise Asset Model is a way to represent the physical assets, such as equipment, - processes, and systems, that exist in an industrial environment. This model provides a structured and - hierarchical representation of these assets, allowing users to define the relationships and properties - of each asset. - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -The Asset Model MyAssetModel already exists. The id of the existing model is ffbc475b-73ad-4eb6-bf28-8728818fa8ef. Moving on... - -Enter 'c' followed by to continue: -c -Continuing with the program... - -------------------------------------------------------------------------------- -2. Create an AWS IoT SiteWise Asset - The IoT SiteWise model defines the structure and metadata for your physical assets. Now we - can use the asset model to create the asset. - +Upload the JSON to the glue-5ffb912c3d534e8493bac675c2a3196d S3 bucket if it does not exist +[ + { + "id": "1", + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + { + "id": "2", + "name": "Bob Smith", + "email": "bob.smith@example.com" + }, + { + "id": "3", + "name": "Charlie Black", + "email": "charlie.black@example.com" + } +] Enter 'c' followed by to continue: c Continuing with the program... -Asset created with ID: 4d681624-a303-46dd-8830-6189790ae915 +SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". +SLF4J: Defaulting to no-operation (NOP) logger implementation +SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. +The JSON exists in glue-5ffb912c3d534e8493bac675c2a3196d Enter 'c' followed by to continue: c @@ -124,17 +125,22 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -3. Retrieve the property ID values - To send data to an asset, we need to get the property ID values for the - Temperature and Humidity properties. +1. Create Schema Mapping +Entity Resolution Schema Mapping aligns and integrates data from +multiple sources by identifying and matching corresponding entities +like customers or products. It unifies schemas, resolves conflicts, +and uses machine learning to link related entities, enabling a +consolidated, accurate view for improved data quality and decision-making. + +In this example, the schema mapping lines up with the fields in the JSON. That is, +it contains these fields: id, name, and email. Enter 'c' followed by to continue: c Continuing with the program... -The Humidity property Id is feb4aba6-55f9-4b00-b366-27b9d7e5a747 -The Temperature property Id is 6cb505aa-6bcc-46f4-a12a-7ca5df8eb028 +Schema Mapping Created Successfully! Enter 'c' followed by to continue: c @@ -142,47 +148,36 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -4. Send data to an AWS IoT SiteWise Asset -By sending data to an IoT SiteWise Asset, you can aggregate data from -multiple sources, normalize the data into a standard format, and store it in a -centralized location. This makes it easier to analyze and gain insights from the data. - -This example demonstrate how to generate sample data and ingest it into the AWS IoT SiteWise asset. - +2. Create an AWS Entity Resolution Workflow. +An Entity Resolution matching workflow identifies and links records +across datasets that represent the same real-world entity, such as +customers or products. Using techniques like schema mapping, +data profiling, and machine learning algorithms, +it evaluates attributes like names or emails to detect duplicates +or relationships, even with variations or inconsistencies. +The workflow outputs consolidated, de-duplicated data, Enter 'c' followed by to continue: c Continuing with the program... -Data sent successfully. +Workflow created successfully. +The workflow ARN is: arn:aws:entityresolution:us-east-1:814548047983:matchingworkflow/MyMatchingWorkflow450 Enter 'c' followed by to continue: c Continuing with the program... -------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -5. Retrieve the value of the IoT SiteWise Asset property -IoT SiteWise is an AWS service that allows you to collect, process, and analyze industrial data -from connected equipment and sensors. One of the key benefits of reading an IoT SiteWise property -is the ability to gain valuable insights from your industrial data. - - +3. Start the matching job of the MyMatchingWorkflow450 workflow. Enter 'c' followed by to continue: c Continuing with the program... -The property name is: Temperature property -The value of this property is 23.5 - -Enter 'c' followed by to continue: -c -Continuing with the program... - -The property name is: Humidity property -The value of this property is 65.0 +Job ID: ec2dbd1717624b2b806ed93a04c20049 +The matching job was successfully started. Enter 'c' followed by to continue: c @@ -190,93 +185,48 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -6. Create an IoT SiteWise Portal - An IoT SiteWise Portal allows you to aggregate data from multiple industrial sources, - such as sensors, equipment, and control systems, into a centralized platform. - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Portal created successfully. Portal ID 63e65729-b7a1-410a-aa36-94145fe92153 -The portal Id is 63e65729-b7a1-410a-aa36-94145fe92153 +4. Get details for job ec2dbd1717624b2b806ed93a04c20049 Enter 'c' followed by to continue: c Continuing with the program... +Job status: QUEUED +Job details: GetMatchingJobResponse(JobId=ec2dbd1717624b2b806ed93a04c20049, StartTime=2025-01-30T18:37:57.475Z, Status=QUEUED) -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -7. Describe the Portal - In this step, we will describe the step and provide the portal URL. - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Portal URL: https://p-fy9qnrqy.app.iotsitewise.aws +5. Get Schema Mapping. Enter 'c' followed by to continue: c Continuing with the program... +Attribute Name: id, Attribute Type: UNIQUE_ID +Attribute Name: name, Attribute Type: STRING +Attribute Name: email, Attribute Type: STRING +Schema mapping retrieval completed. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -8. Create an IoTSitewise Gateway -IoTSitewise Gateway serves as the bridge between industrial equipment, sensors, and the -cloud-based IoTSitewise service. It is responsible for securely collecting, processing, and -transmitting data from various industrial assets to the IoTSitewise platform, -enabling real-time monitoring, analysis, and optimization of industrial operations. - - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -The ARN of the gateway is arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 -Gateway creation completed successfully. id is 50320670-1d88-4a7e-9013-1d7e8a3af832 +6. List Schema Mappings. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -9. Describe the IoTSitewise Gateway +7. Tag the schema450resource. +Tags can help you organize and categorize your Entity Resolution resources. +You can also use them to scope user permissions by granting a user permission +to access or change only resources with certain tag values. +In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, +the SchemaMapping is tagged. -Enter 'c' followed by to continue: -c -Continuing with the program... +Successfully tagged the resource. -Gateway Name: myGateway11 -Gateway ARN: arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 -Gateway Platform: GatewayPlatform(GreengrassV2=GreengrassV2(CoreDeviceThingName=myThing78)) -Gateway Creation Date: 2024-09-18T20:34:13.117Z -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -10. Delete the AWS IoT SiteWise Assets -Before you can delete the Asset Model, you must delete the assets. - - -Would you like to delete the IoT Sitewise Assets? (y/n) -y -You selected to delete the Sitewise assets. - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Portal 63e65729-b7a1-410a-aa36-94145fe92153 was deleted successfully. -An unexpected error occurred: Cannot invoke "java.util.concurrent.CompletableFuture.join()" because "future" is null -Asset deleted successfully. -Lets wait 1 min for the asset to be deleted -01:00The Gateway was deleted successfully -00:00Countdown complete! - -Enter 'c' followed by to continue: -c -Continuing with the program... +8. Delete the AWS Entity Resolution Workflow. +You cannot delete a workflow that is in a running state. +Would you like to wait for the workflow to complete. +This can take up to 30 mins (y/n). -Delete the AWS IoT SiteWise Asset Model -Asset model deleted successfully. +n Enter 'c' followed by to continue: c @@ -284,9 +234,7 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -Delete stack requested .... -Stack deleted successfully. -This concludes the AWS SiteWise Scenario +This concludes the AWS Entity Resolution scenario. -------------------------------------------------------------------------------- From 545c19ffdfdd4b02e7e1814d8ce8f3d4cd02b4f9 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 15:51:47 -0500 Subject: [PATCH 007/144] added a readme --- scenarios/basics/entity_resolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 9df7e209432..001e5a6a87b 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -1,5 +1,5 @@ ## Overview -This AWS IoT SiteWise Service basic scenario demonstrates how to interact with the AWS IoT SiteWise service using an AWS SDK. The scenario covers various operations such as creating an Asset Model, creating assets, sending data to assets, and retrieving data. +This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. The scenario covers various operations such as creating a schema mapping, creating a matching workflow, starting a matching job, and so on. ## Key Operations From dd2f7bde8ce527554e0dfa8f2d542c4190fa1f94 Mon Sep 17 00:00:00 2001 From: Scott Macdonald <57190223+scmacdon@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:54:18 -0500 Subject: [PATCH 008/144] Kotlin SDK update the build version in Kotlin SDK (#7204) --- kotlin/services/apigateway/build.gradle.kts | 9 +- kotlin/services/appsync/build.gradle.kts | 13 +- kotlin/services/athena/build.gradle.kts | 9 +- kotlin/services/autoscale/build.gradle.kts | 9 +- .../services/cloudformation/build.gradle.kts | 9 +- .../com/kotlin/cloudformation/CreateStack.kt | 14 +- .../src/test/kotlin/CloudFormationTest.kt | 4 +- kotlin/services/cloudtrail/build.gradle.kts | 9 +- kotlin/services/cloudwatch/build.gradle.kts | 13 +- kotlin/services/codepipeline/build.gradle.kts | 9 +- kotlin/services/cognito/build.gradle.kts | 11 +- .../src/test/kotlin/CognitoKotlinTest.kt | 41 ++---- kotlin/services/comprehend/build.gradle.kts | 9 +- kotlin/services/dynamodb/build.gradle.kts | 9 +- kotlin/services/ec2/build.gradle.kts | 11 +- .../services/ec2/src/test/kotlin/EC2Test.kt | 137 ------------------ kotlin/services/ecr/build.gradle.kts | 9 +- kotlin/services/ecs/build.gradle.kts | 9 +- .../services/ecs/src/test/kotlin/ESCTest.kt | 11 +- .../elasticbeanstalk/build.gradle.kts | 9 +- kotlin/services/emr/build.gradle.kts | 9 +- kotlin/services/eventbridge/build.gradle.kts | 13 +- kotlin/services/firehose/build.gradle.kts | 9 +- kotlin/services/forecast/build.gradle.kts | 9 +- kotlin/services/glue/build.gradle.kts | 9 +- .../kotlin/com/kotlin/glue/SearchTables.kt | 2 +- .../services/glue/src/test/kotlin/GlueTest.kt | 8 +- kotlin/services/iam/build.gradle.kts | 13 +- .../services/iam/src/test/kotlin/IAMTest.kt | 6 +- kotlin/services/iot/build.gradle.kts | 11 +- kotlin/services/kendra/build.gradle.kts | 11 +- kotlin/services/keyspaces/build.gradle.kts | 7 +- kotlin/services/kinesis/build.gradle.kts | 7 +- kotlin/services/kms/README.md | 6 +- kotlin/services/kms/build.gradle.kts | 9 +- .../kotlin/com/kotlin/kms/EncryptDataKey.kt | 15 +- .../kms/src/test/kotlin/KMSKotlinTest.kt | 6 +- kotlin/services/lambda/build.gradle.kts | 9 +- .../com/kotlin/lambda/CreateFunction.kt | 4 +- .../com/kotlin/lambda/DeleteFunction.kt | 2 +- .../com/kotlin/lambda/LambdaScenario.kt | 18 +-- .../lambda/src/test/kotlin/LambdaTest.kt | 2 +- kotlin/services/lex/build.gradle.kts | 11 +- .../kotlin/com/kotlin/lex/GetBotStatus.kt | 2 +- kotlin/services/mediaconvert/build.gradle.kts | 9 +- .../mediaconvert/src/test/kotlin/MCTest.kt | 3 + kotlin/services/mediastore/build.gradle.kts | 7 +- kotlin/services/opensearch/build.gradle.kts | 7 +- kotlin/services/personalize/build.gradle.kts | 11 +- kotlin/services/pinpoint/build.gradle.kts | 11 +- kotlin/services/polly/build.gradle.kts | 7 +- kotlin/services/rds/build.gradle.kts | 9 +- .../kotlin/com/kotlin/rds/CreateDBInstance.kt | 6 +- .../main/kotlin/com/kotlin/rds/RDSScenario.kt | 6 +- kotlin/services/redshift/README.md | 4 +- kotlin/services/redshift/build.gradle.kts | 9 +- .../kotlin/redshift/CreateAndModifyCluster.kt | 1 + .../src/test/kotlin/RedshiftKotlinTest.kt | 60 +------- kotlin/services/rekognition/build.gradle.kts | 9 +- .../kotlin/rekognition/VideoDetectFaces.kt | 2 +- kotlin/services/route53/build.gradle.kts | 11 +- kotlin/services/s3/build.gradle.kts | 2 +- kotlin/services/sagemaker/build.gradle.kts | 9 +- .../services/secrets-manager/build.gradle.kts | 9 +- kotlin/services/ses/build.gradle.kts | 9 +- .../services/ses/src/test/kotlin/SESTest.kt | 9 -- kotlin/services/sns/build.gradle.kts | 9 +- kotlin/services/sqs/build.gradle.kts | 9 +- .../services/stepfunctions/build.gradle.kts | 11 +- .../com/kotlin/stepfunctions/GetStream.kt | 5 +- .../stepfunctions/StepFunctionsScenario.kt | 12 +- .../test/kotlin/StepFunctionsKotlinTest.kt | 122 +--------------- kotlin/services/sts/build.gradle.kts | 9 +- kotlin/services/support/build.gradle.kts | 22 ++- kotlin/services/textract/build.gradle.kts | 9 +- .../kotlin/textract/StartDocumentAnalysis.kt | 2 +- kotlin/services/translate/build.gradle.kts | 9 +- kotlin/services/xray/build.gradle.kts | 9 +- .../src/test/kotlin/TranslateKotlinTest.kt | 114 --------------- 79 files changed, 361 insertions(+), 743 deletions(-) delete mode 100644 kotlin/services/xray/src/test/kotlin/TranslateKotlinTest.kt diff --git a/kotlin/services/apigateway/build.gradle.kts b/kotlin/services/apigateway/build.gradle.kts index 96b473cfbc8..26b3b97dc40 100644 --- a/kotlin/services/apigateway/build.gradle.kts +++ b/kotlin/services/apigateway/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:apigateway:1.0.30") - implementation("aws.sdk.kotlin:secretsmanager:1.0.30") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:apigateway") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") diff --git a/kotlin/services/appsync/build.gradle.kts b/kotlin/services/appsync/build.gradle.kts index df71aa24005..5b8b1111486 100644 --- a/kotlin/services/appsync/build.gradle.kts +++ b/kotlin/services/appsync/build.gradle.kts @@ -27,12 +27,13 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:appsync:1.2.28") - implementation("aws.sdk.kotlin:sts:1.2.28") - implementation("aws.sdk.kotlin:s3:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:appsync") + implementation("aws.sdk.kotlin:sts") + implementation("aws.sdk.kotlin:s3") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") implementation("com.googlecode.json-simple:json-simple:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") diff --git a/kotlin/services/athena/build.gradle.kts b/kotlin/services/athena/build.gradle.kts index e1fbffb801a..1e517fd31ea 100644 --- a/kotlin/services/athena/build.gradle.kts +++ b/kotlin/services/athena/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:athena:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:athena") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/autoscale/build.gradle.kts b/kotlin/services/autoscale/build.gradle.kts index 7a60f9db76e..e5143ef9274 100644 --- a/kotlin/services/autoscale/build.gradle.kts +++ b/kotlin/services/autoscale/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:autoscaling:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:autoscaling") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/cloudformation/build.gradle.kts b/kotlin/services/cloudformation/build.gradle.kts index 1dc9c01ce78..70a4f330a59 100644 --- a/kotlin/services/cloudformation/build.gradle.kts +++ b/kotlin/services/cloudformation/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:cloudformation:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:cloudformation") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/cloudformation/src/main/kotlin/com/kotlin/cloudformation/CreateStack.kt b/kotlin/services/cloudformation/src/main/kotlin/com/kotlin/cloudformation/CreateStack.kt index de3e514054b..2d0f25024ec 100644 --- a/kotlin/services/cloudformation/src/main/kotlin/com/kotlin/cloudformation/CreateStack.kt +++ b/kotlin/services/cloudformation/src/main/kotlin/com/kotlin/cloudformation/CreateStack.kt @@ -7,7 +7,6 @@ package com.kotlin.cloudformation import aws.sdk.kotlin.services.cloudformation.CloudFormationClient import aws.sdk.kotlin.services.cloudformation.model.CreateStackRequest import aws.sdk.kotlin.services.cloudformation.model.OnFailure -import aws.sdk.kotlin.services.cloudformation.model.Parameter import kotlin.system.exitProcess // snippet-end:[cf.kotlin.create_stack.import] @@ -32,26 +31,17 @@ suspend fun main(args: Array) { val stackName = args[0] val roleARN = args[1] val location = args[2] - val key = args[3] - val value = args[4] - createCFStack(stackName, roleARN, location, key, value) + createCFStack(stackName, roleARN, location) } // snippet-start:[cf.kotlin.create_stack.main] -suspend fun createCFStack(stackNameVal: String, roleARNVal: String?, location: String?, key: String?, value: String?) { - val myParameter = - Parameter { - parameterKey = key - parameterValue = value - } - +suspend fun createCFStack(stackNameVal: String, roleARNVal: String?, location: String?) { val request = CreateStackRequest { stackName = stackNameVal templateUrl = location roleArn = roleARNVal onFailure = OnFailure.Rollback - parameters = listOf(myParameter) } CloudFormationClient { region = "us-east-1" }.use { cfClient -> diff --git a/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt b/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt index c5ae594ffad..ae572c94973 100644 --- a/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt +++ b/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt @@ -38,8 +38,6 @@ class CloudFormationTest { stackName = values.stackName.toString() roleARN = values.roleARN.toString() location = values.location.toString() - key = values.key.toString() - value = values.value.toString() /* val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") @@ -57,7 +55,7 @@ class CloudFormationTest { @Order(1) fun createStackTest() = runBlocking { - createCFStack(stackName, roleARN, location, key, value) + createCFStack(stackName, roleARN, location) println("Test 1 passed") } diff --git a/kotlin/services/cloudtrail/build.gradle.kts b/kotlin/services/cloudtrail/build.gradle.kts index a4dc36dc59e..1ea9ca8fe62 100644 --- a/kotlin/services/cloudtrail/build.gradle.kts +++ b/kotlin/services/cloudtrail/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:cloudtrail:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:cloudtrail") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/cloudwatch/build.gradle.kts b/kotlin/services/cloudwatch/build.gradle.kts index 8a3484df187..5715a0a9f02 100644 --- a/kotlin/services/cloudwatch/build.gradle.kts +++ b/kotlin/services/cloudwatch/build.gradle.kts @@ -28,12 +28,13 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:cloudwatch:1.2.28") - implementation("aws.sdk.kotlin:cloudwatchevents:1.2.28") - implementation("aws.sdk.kotlin:cloudwatchlogs:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:cloudwatch") + implementation("aws.sdk.kotlin:cloudwatchevents") + implementation("aws.sdk.kotlin:cloudwatchlogs") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") implementation("com.fasterxml.jackson.core:jackson-core:2.14.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") diff --git a/kotlin/services/codepipeline/build.gradle.kts b/kotlin/services/codepipeline/build.gradle.kts index b52524bfc71..367b95dc0a4 100644 --- a/kotlin/services/codepipeline/build.gradle.kts +++ b/kotlin/services/codepipeline/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:codepipeline:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:codepipeline") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/cognito/build.gradle.kts b/kotlin/services/cognito/build.gradle.kts index b57f2aaf83d..57f51b4bef7 100644 --- a/kotlin/services/cognito/build.gradle.kts +++ b/kotlin/services/cognito/build.gradle.kts @@ -27,11 +27,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:cognitoidentityprovider:1.2.28") - implementation("aws.sdk.kotlin:cognitoidentity:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:cognitoidentityprovider") + implementation("aws.sdk.kotlin:cognitoidentity") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("com.google.code.gson:gson:2.10") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt b/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt index 1b40f078fc1..2d5b06ca5d6 100644 --- a/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt +++ b/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt @@ -15,7 +15,6 @@ import com.kotlin.cognito.getAllPools import com.kotlin.cognito.getPools import com.kotlin.cognito.listAllUserPoolClients import com.kotlin.cognito.listPoolIdentities -import com.kotlin.cognito.signUp import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll @@ -127,23 +126,23 @@ class CognitoKotlinTest { @Test @Order(3) - fun signUpUserTest() = + fun listUserPoolsTest() = runBlocking { - signUp(clientId, secretkey, username, password, email) + getAllPools() println("Test 3 passed") } @Test @Order(4) - fun listUserPoolsTest() = + fun listUserPoolClientsTest() = runBlocking { - getAllPools() + listAllUserPoolClients(existingUserPoolId) println("Test 4 passed") } @Test @Order(5) - fun listUserPoolClientsTest() = + fun listUsersTest() = runBlocking { listAllUserPoolClients(existingUserPoolId) println("Test 5 passed") @@ -151,59 +150,51 @@ class CognitoKotlinTest { @Test @Order(6) - fun listUsersTest() = - runBlocking { - listAllUserPoolClients(existingUserPoolId) - println("Test 6 passed") - } - - @Test - @Order(7) fun describeUserPoolTest() = runBlocking { describePool(existingUserPoolId) - println("Test 7 passed") + println("Test 6 passed") } @Test - @Order(8) + @Order(7) fun deleteUserPool() = runBlocking { delPool(userPoolId) - println("Test 8 passed") + println("Test 7 passed") } @Test - @Order(9) + @Order(8) fun createIdentityPoolTest() = runBlocking { identityPoolId = createIdPool(identityPoolName).toString() Assertions.assertTrue(!identityPoolId.isEmpty()) - println("Test 9 passed") + println("Test 8 passed") } @Test - @Order(10) + @Order(9) fun listIdentityProvidersTest() = runBlocking { getPools() - println("Test 10 passed") + println("Test 9 passed") } @Test - @Order(11) + @Order(10) fun listIdentitiesTest() = runBlocking { listPoolIdentities(identityPoolId) - println("Test 11 passed") + println("Test 10 passed") } @Test - @Order(12) + @Order(11) fun deleteIdentityPool() = runBlocking { deleteIdPool(identityPoolId) - println("Test 12 passed") + println("Test 11 passed") } private suspend fun getSecretValues(): String { diff --git a/kotlin/services/comprehend/build.gradle.kts b/kotlin/services/comprehend/build.gradle.kts index 3b798201a6c..28ca4042626 100644 --- a/kotlin/services/comprehend/build.gradle.kts +++ b/kotlin/services/comprehend/build.gradle.kts @@ -29,10 +29,11 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:comprehend:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:comprehend") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/dynamodb/build.gradle.kts b/kotlin/services/dynamodb/build.gradle.kts index 7afc034c5c3..1d6bfc1f817 100644 --- a/kotlin/services/dynamodb/build.gradle.kts +++ b/kotlin/services/dynamodb/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:dynamodb:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:dynamodb") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation(kotlin("reflect")) diff --git a/kotlin/services/ec2/build.gradle.kts b/kotlin/services/ec2/build.gradle.kts index d764ee73b7a..9643febab98 100644 --- a/kotlin/services/ec2/build.gradle.kts +++ b/kotlin/services/ec2/build.gradle.kts @@ -28,11 +28,12 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:ec2:1.2.28") - implementation("aws.sdk.kotlin:ssm:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:ec2") + implementation("aws.sdk.kotlin:ssm") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/ec2/src/test/kotlin/EC2Test.kt b/kotlin/services/ec2/src/test/kotlin/EC2Test.kt index 89b12ce41cd..aed25c8bd84 100644 --- a/kotlin/services/ec2/src/test/kotlin/EC2Test.kt +++ b/kotlin/services/ec2/src/test/kotlin/EC2Test.kt @@ -5,39 +5,20 @@ import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson -import com.kotlin.ec2.DASHES -import com.kotlin.ec2.allocateAddressSc -import com.kotlin.ec2.associateAddressSc import com.kotlin.ec2.createEC2Instance import com.kotlin.ec2.createEC2KeyPair import com.kotlin.ec2.createEC2SecurityGroup -import com.kotlin.ec2.createEC2SecurityGroupSc -import com.kotlin.ec2.createKeyPairSc import com.kotlin.ec2.deleteEC2SecGroup -import com.kotlin.ec2.deleteEC2SecGroupSc import com.kotlin.ec2.deleteKeys -import com.kotlin.ec2.deleteKeysSc import com.kotlin.ec2.describeEC2Account import com.kotlin.ec2.describeEC2Address import com.kotlin.ec2.describeEC2Instances -import com.kotlin.ec2.describeEC2InstancesSc import com.kotlin.ec2.describeEC2Keys -import com.kotlin.ec2.describeEC2KeysSc import com.kotlin.ec2.describeEC2RegionsAndZones import com.kotlin.ec2.describeEC2SecurityGroups import com.kotlin.ec2.describeEC2Vpcs -import com.kotlin.ec2.describeImageSc -import com.kotlin.ec2.describeSecurityGroupsSc -import com.kotlin.ec2.disassociateAddressSc import com.kotlin.ec2.findRunningEC2Instances -import com.kotlin.ec2.getInstanceTypesSc -import com.kotlin.ec2.getParaValuesSc -import com.kotlin.ec2.releaseEC2AddressSc -import com.kotlin.ec2.runInstanceSc -import com.kotlin.ec2.startInstanceSc -import com.kotlin.ec2.stopInstanceSc import com.kotlin.ec2.terminateEC2 -import com.kotlin.ec2.terminateEC2Sc import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll @@ -240,124 +221,6 @@ class EC2Test { println("Test 14 passed") } - @Test - @Order(15) - fun fullEC2ScenarioTest() = - runBlocking { - var newInstanceId: String - println(DASHES) - println("1. Create an RSA key pair and save the private key material as a .pem file.") - createKeyPairSc(keyNameSc, fileNameSc) - println(DASHES) - - println(DASHES) - println("2. List key pairs.") - describeEC2KeysSc() - println(DASHES) - - println(DASHES) - println("3. Create a security group.") - val groupId = createEC2SecurityGroupSc(groupNameSc, groupDescSc, vpcIdSc, myIpAddressSc) - groupId?.let { assertTrue(it.isNotEmpty()) } - println(DASHES) - - println(DASHES) - println("4. Display security group info for the newly created security group.") - describeSecurityGroupsSc(groupId.toString()) - println(DASHES) - - println(DASHES) - println("5. Get a list of Amazon Linux 2 AMIs and select one with amzn2 in the name.") - val instanceId = getParaValuesSc() - instanceId?.let { assertTrue(it.isNotEmpty()) } - println("The instance ID is $instanceId") - println(DASHES) - - println(DASHES) - println("6. Get more information about an amzn2 image and return the AMI value.") - val amiValue = instanceId?.let { describeImageSc(it) } - amiValue?.let { assertTrue(it.isNotEmpty()) } - println("The AMI value is $amiValue.") - println(DASHES) - - println(DASHES) - println("7. Get a list of instance types.") - var instanceType = getInstanceTypesSc() - assertTrue(instanceType.isNotEmpty()) - println(DASHES) - - println(DASHES) - println("8. Create an instance.") - instanceType = "m5.large" - println("Wait 1 min before creating the instance using $instanceType") - // TimeUnit.MINUTES.sleep(1) - newInstanceId = runInstanceSc(instanceType, keyNameSc, groupNameSc, amiValue.toString()) - assertTrue(newInstanceId.isNotEmpty()) - println(DASHES) - - println(DASHES) - println("9. Display information about the running instance.") - var ipAddress = describeEC2InstancesSc(newInstanceId) - assertTrue(ipAddress.isNotEmpty()) - println("You can SSH to the instance using this command:") - println("ssh -i " + fileNameSc + "ec2-user@" + ipAddress) - println(DASHES) - - println(DASHES) - println("10. Stop the instance.") - stopInstanceSc(newInstanceId) - println(DASHES) - - println(DASHES) - println("11. Start the instance.") - startInstanceSc(newInstanceId) - ipAddress = describeEC2InstancesSc(newInstanceId) - ipAddress.let { assertTrue(it.isNotEmpty()) } - println("You can SSH to the instance using this command:") - println("ssh -i " + fileNameSc + "ec2-user@" + ipAddress) - println(DASHES) - - println(DASHES) - println("12. Allocate an Elastic IP and associate it with the instance.") - val allocationId = allocateAddressSc() - allocationId?.let { assertTrue(it.isNotEmpty()) } - val associationId = associateAddressSc(newInstanceId, allocationId) - associationId?.let { assertTrue(it.isNotEmpty()) } - println("The associate Id value is $associationId") - println(DASHES) - - println(DASHES) - println("13. Describe the instance again.") - ipAddress = describeEC2InstancesSc(newInstanceId) - ipAddress.let { assertTrue(it.isNotEmpty()) } - println("You can SSH to the instance using this command:") - println("ssh -i " + fileNameSc + "ec2-user@" + ipAddress) - println(DASHES) - - println(DASHES) - println("14. Disassociate and release the Elastic IP address.") - disassociateAddressSc(associationId) - releaseEC2AddressSc(allocationId) - println(DASHES) - - println(DASHES) - println("15. Terminate the instance and use a waiter.") - terminateEC2Sc(newInstanceId) - println(DASHES) - - println(DASHES) - println("16. Delete the security group.") - if (groupId != null) { - deleteEC2SecGroupSc(groupId) - } - println(DASHES) - - println(DASHES) - println("17. Delete the key pair.") - deleteKeysSc(keyNameSc) - println(DASHES) - } - private suspend fun getSecretValues(): String { val secretName = "test/ec2" val valueRequest = diff --git a/kotlin/services/ecr/build.gradle.kts b/kotlin/services/ecr/build.gradle.kts index 2373c2b1a25..95172f834b0 100644 --- a/kotlin/services/ecr/build.gradle.kts +++ b/kotlin/services/ecr/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:ecr:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:ecr") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/ecs/build.gradle.kts b/kotlin/services/ecs/build.gradle.kts index 5c84d3828aa..87e01bffab3 100644 --- a/kotlin/services/ecs/build.gradle.kts +++ b/kotlin/services/ecs/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:ecs:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:ecs") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.google.code.gson:gson:2.10") diff --git a/kotlin/services/ecs/src/test/kotlin/ESCTest.kt b/kotlin/services/ecs/src/test/kotlin/ESCTest.kt index 5fda8e76bd3..2299866da3e 100644 --- a/kotlin/services/ecs/src/test/kotlin/ESCTest.kt +++ b/kotlin/services/ecs/src/test/kotlin/ESCTest.kt @@ -34,7 +34,6 @@ class ESCTest { var serviceName: String = "" var serviceArn: String = "" var taskDefinition: String = "" - var clusterArn: String = "arn:aws:ecs:us-east-1:814548047983:cluster/ScottCluste11" @BeforeAll fun setup() = @@ -78,7 +77,7 @@ class ESCTest { @Order(2) fun createServiceTest() = runBlocking { - serviceArn = createNewService(clusterArn, serviceName, securityGroups, subnet, taskDefinition).toString() + serviceArn = createNewService(clusterARN, serviceName, securityGroups, subnet, taskDefinition).toString() println("Test 2 passed") } @@ -94,7 +93,7 @@ class ESCTest { @Order(4) fun describeClustersTest() = runBlocking { - descCluster(clusterArn) + descCluster(clusterARN) println("Test 4 passed") } @@ -102,7 +101,7 @@ class ESCTest { @Order(5) fun listTaskDefinitionsTest() = runBlocking { - getAllTasks(clusterArn, taskId) + getAllTasks(clusterARN, taskId) println("Test 5 passed") } @@ -110,7 +109,7 @@ class ESCTest { @Order(6) fun updateServiceTest() = runBlocking { - updateSpecificService(clusterArn, serviceArn) + updateSpecificService(clusterARN, serviceArn) println("Test 6 passed") } @@ -118,7 +117,7 @@ class ESCTest { @Order(7) fun deleteServiceTest() = runBlocking { - deleteSpecificService(clusterArn, serviceArn) + deleteSpecificService(clusterARN, serviceArn) println("Test 7 passed") } diff --git a/kotlin/services/elasticbeanstalk/build.gradle.kts b/kotlin/services/elasticbeanstalk/build.gradle.kts index 3ebf39a5304..fa56641efc2 100644 --- a/kotlin/services/elasticbeanstalk/build.gradle.kts +++ b/kotlin/services/elasticbeanstalk/build.gradle.kts @@ -28,10 +28,11 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:elasticbeanstalk:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:elasticbeanstalk") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") } diff --git a/kotlin/services/emr/build.gradle.kts b/kotlin/services/emr/build.gradle.kts index f15411d7126..83e8ca4b359 100644 --- a/kotlin/services/emr/build.gradle.kts +++ b/kotlin/services/emr/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:emr:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:emr") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/eventbridge/build.gradle.kts b/kotlin/services/eventbridge/build.gradle.kts index b0ddbf2683a..07286ad0d75 100644 --- a/kotlin/services/eventbridge/build.gradle.kts +++ b/kotlin/services/eventbridge/build.gradle.kts @@ -28,12 +28,13 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:eventbridge:1.2.28") - implementation("aws.sdk.kotlin:iam:1.2.28") - implementation("aws.sdk.kotlin:sns:1.2.28") - implementation("aws.sdk.kotlin:s3:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:eventbridge") + implementation("aws.sdk.kotlin:iam") + implementation("aws.sdk.kotlin:sns") + implementation("aws.sdk.kotlin:s3") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") } diff --git a/kotlin/services/firehose/build.gradle.kts b/kotlin/services/firehose/build.gradle.kts index 937d8ec3a90..8c70d22d4be 100644 --- a/kotlin/services/firehose/build.gradle.kts +++ b/kotlin/services/firehose/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:firehose:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:firehose") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") diff --git a/kotlin/services/forecast/build.gradle.kts b/kotlin/services/forecast/build.gradle.kts index 6f962cb5c73..41c296a2c09 100644 --- a/kotlin/services/forecast/build.gradle.kts +++ b/kotlin/services/forecast/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:forecast:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:forecast") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/glue/build.gradle.kts b/kotlin/services/glue/build.gradle.kts index 792fc5eb9fe..6b562da1edb 100644 --- a/kotlin/services/glue/build.gradle.kts +++ b/kotlin/services/glue/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:glue:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:glue") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/glue/src/main/kotlin/com/kotlin/glue/SearchTables.kt b/kotlin/services/glue/src/main/kotlin/com/kotlin/glue/SearchTables.kt index c06167c8c4f..a3190933dc2 100644 --- a/kotlin/services/glue/src/main/kotlin/com/kotlin/glue/SearchTables.kt +++ b/kotlin/services/glue/src/main/kotlin/com/kotlin/glue/SearchTables.kt @@ -40,7 +40,7 @@ suspend fun searchGlueTable(text: String?) { val request = SearchTablesRequest { searchText = text - resourceShareType = ResourceShareType.fromValue("All") + resourceShareType = ResourceShareType.All maxResults = 10 } diff --git a/kotlin/services/glue/src/test/kotlin/GlueTest.kt b/kotlin/services/glue/src/test/kotlin/GlueTest.kt index 7ecc579e5f1..1dfc4b10781 100644 --- a/kotlin/services/glue/src/test/kotlin/GlueTest.kt +++ b/kotlin/services/glue/src/test/kotlin/GlueTest.kt @@ -73,7 +73,7 @@ class GlueTest { } @Test - @Order(2) + @Order(1) fun getCrawlersTest() = runBlocking { getAllCrawlers() @@ -81,7 +81,7 @@ class GlueTest { } @Test - @Order(4) + @Order(2) fun getDatabasesTest() = runBlocking { getAllDatabases() @@ -89,7 +89,7 @@ class GlueTest { } @Test - @Order(5) + @Order(3) fun searchTablesTest() = runBlocking { searchGlueTable(text) @@ -97,7 +97,7 @@ class GlueTest { } @Test - @Order(6) + @Order(4) fun listWorkflowsTest() = runBlocking { listAllWorkflows() diff --git a/kotlin/services/iam/build.gradle.kts b/kotlin/services/iam/build.gradle.kts index 8febd761f60..40ba2d14ed2 100644 --- a/kotlin/services/iam/build.gradle.kts +++ b/kotlin/services/iam/build.gradle.kts @@ -27,12 +27,13 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:iam:1.2.28") - implementation("aws.sdk.kotlin:sts:1.2.28") - implementation("aws.sdk.kotlin:s3:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:iam") + implementation("aws.sdk.kotlin:sts") + implementation("aws.sdk.kotlin:s3") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") implementation("com.googlecode.json-simple:json-simple:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") diff --git a/kotlin/services/iam/src/test/kotlin/IAMTest.kt b/kotlin/services/iam/src/test/kotlin/IAMTest.kt index 0be47acf2b2..c755fb05b04 100644 --- a/kotlin/services/iam/src/test/kotlin/IAMTest.kt +++ b/kotlin/services/iam/src/test/kotlin/IAMTest.kt @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import kotlin.random.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) @@ -51,9 +52,10 @@ class IAMTest { // Get the values to run these tests from AWS Secrets Manager. val gson = Gson() val json: String = getSecretValues() + val randomValue = Random.nextInt(1, 10001) val values = gson.fromJson(json, SecretValues::class.java) - userName = values.userName.toString() - policyName = values.policyName.toString() + userName = values.userName.toString() + randomValue + policyName = values.policyName.toString() + randomValue roleName = values.roleName.toString() accountAlias = values.accountAlias.toString() usernameSc = values.usernameSc.toString() diff --git a/kotlin/services/iot/build.gradle.kts b/kotlin/services/iot/build.gradle.kts index 9892e3970d2..7f1c544d2b0 100644 --- a/kotlin/services/iot/build.gradle.kts +++ b/kotlin/services/iot/build.gradle.kts @@ -27,11 +27,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:iot:1.2.28") - implementation("aws.sdk.kotlin:iotdataplane:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:iot") + implementation("aws.sdk.kotlin:iotdataplane") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/kendra/build.gradle.kts b/kotlin/services/kendra/build.gradle.kts index 1b424d340b9..6f8ea8043f3 100644 --- a/kotlin/services/kendra/build.gradle.kts +++ b/kotlin/services/kendra/build.gradle.kts @@ -28,11 +28,12 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:kendra:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:kendra") + implementation("aws.sdk.kotlin:secretsmanager") + testImplementation("org.junit.jupiter:junit-jupiter") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.google.code.gson:gson:2.10") diff --git a/kotlin/services/keyspaces/build.gradle.kts b/kotlin/services/keyspaces/build.gradle.kts index a2771f4f4ef..01946318370 100644 --- a/kotlin/services/keyspaces/build.gradle.kts +++ b/kotlin/services/keyspaces/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:keyspaces:1.2.28") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:keyspaces") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.datastax.oss:java-driver-core:4.15.0") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") implementation("software.aws.mcs:aws-sigv4-auth-cassandra-java-driver-plugin:4.0.8") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/kinesis/build.gradle.kts b/kotlin/services/kinesis/build.gradle.kts index 12a5cd6b41f..cfa3a38a304 100644 --- a/kotlin/services/kinesis/build.gradle.kts +++ b/kotlin/services/kinesis/build.gradle.kts @@ -27,9 +27,10 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:kinesis:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:kinesis") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.0") diff --git a/kotlin/services/kms/README.md b/kotlin/services/kms/README.md index 727598b7064..5e8e51329d4 100644 --- a/kotlin/services/kms/README.md +++ b/kotlin/services/kms/README.md @@ -36,11 +36,11 @@ Code excerpts that show you how to call individual service functions. - [CreateAlias](src/main/kotlin/com/kotlin/kms/CreateAlias.kt#L39) - [CreateGrant](src/main/kotlin/com/kotlin/kms/CreateGrant.kt#L43) - [CreateKey](src/main/kotlin/com/kotlin/kms/CreateCustomerKey.kt#L27) -- [Decrypt](src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt#L42) +- [Decrypt](src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt#L40) - [DescribeKey](src/main/kotlin/com/kotlin/kms/DescribeKey.kt#L37) - [DisableKey](src/main/kotlin/com/kotlin/kms/DisableCustomerKey.kt#L37) - [EnableKey](src/main/kotlin/com/kotlin/kms/EnableCustomerKey.kt#L37) -- [Encrypt](src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt#L42) +- [Encrypt](src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt#L40) - [ListAliases](src/main/kotlin/com/kotlin/kms/ListAliases.kt#L23) - [ListGrants](src/main/kotlin/com/kotlin/kms/ListGrants.kt#L36) - [ListKeys](src/main/kotlin/com/kotlin/kms/ListKeys.kt#L22) @@ -94,4 +94,4 @@ in the `kotlin` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/kotlin/services/kms/build.gradle.kts b/kotlin/services/kms/build.gradle.kts index 7807a201e2f..da517edbbec 100644 --- a/kotlin/services/kms/build.gradle.kts +++ b/kotlin/services/kms/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:kms:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:kms") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/kms/src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt b/kotlin/services/kms/src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt index cb7478eee19..0938dc040ce 100644 --- a/kotlin/services/kms/src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt +++ b/kotlin/services/kms/src/main/kotlin/com/kotlin/kms/EncryptDataKey.kt @@ -7,8 +7,8 @@ package com.kotlin.kms import aws.sdk.kotlin.services.kms.KmsClient import aws.sdk.kotlin.services.kms.model.DecryptRequest import aws.sdk.kotlin.services.kms.model.EncryptRequest -import java.io.File import kotlin.system.exitProcess + // snippet-end:[kms.kotlin_encrypt_data.import] /** @@ -25,18 +25,16 @@ suspend fun main(args: Array) { Where: keyId - A key id value to describe (for example, xxxxxbcd-12ab-34cd-56ef-1234567890ab). - path - The path of a text file where the data is written to (for example, C:\AWS\TextFile.txt). """ - if (args.size != 2) { + if (args.size != 1) { println(usage) exitProcess(0) } val keyId = args[0] - val path = args[1] val encryptedData = encryptData(keyId) - decryptData(encryptedData, keyId, path) + decryptData(encryptedData, keyId) } // snippet-start:[kms.kotlin_encrypt_data.main] @@ -63,7 +61,6 @@ suspend fun encryptData(keyIdValue: String): ByteArray? { suspend fun decryptData( encryptedDataVal: ByteArray?, keyIdVal: String?, - path: String, ) { val decryptRequest = DecryptRequest { @@ -74,10 +71,8 @@ suspend fun decryptData( val decryptResponse = kmsClient.decrypt(decryptRequest) val myVal = decryptResponse.plaintext - // Write the decrypted data to a file. - if (myVal != null) { - File(path).writeBytes(myVal) - } + // Print the decrypted data. + print(myVal) } } // snippet-end:[kms.kotlin_encrypt_data.main] diff --git a/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt b/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt index 9b670c534b5..30a2b04b91d 100644 --- a/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt +++ b/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt @@ -38,7 +38,6 @@ class KMSKotlinTest { private var operation = "" private var grantId = "" private var aliasName = "" - private var path = "" @BeforeAll fun setup() = @@ -51,7 +50,6 @@ class KMSKotlinTest { operation = values.operation.toString() aliasName = values.aliasName.toString() granteePrincipal = values.granteePrincipal.toString() - path = values.path.toString() /* val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") @@ -61,7 +59,6 @@ class KMSKotlinTest { granteePrincipal = prop.getProperty("granteePrincipal") operation = prop.getProperty("operation") aliasName = prop.getProperty("aliasName") - path = prop.getProperty("path") */ } @@ -78,8 +75,9 @@ class KMSKotlinTest { @Order(3) fun encryptDataKeyTest() = runBlocking { + val plaintext = "Hello, AWS KMS!" val encryptData = encryptData(keyId) - decryptData(encryptData, keyId, path) + decryptData(encryptData, keyId) println("Test 3 passed") } diff --git a/kotlin/services/lambda/build.gradle.kts b/kotlin/services/lambda/build.gradle.kts index dcc5a825436..914003faa52 100644 --- a/kotlin/services/lambda/build.gradle.kts +++ b/kotlin/services/lambda/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:lambda:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:lambda") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/CreateFunction.kt b/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/CreateFunction.kt index b3639985113..fbf88aa6a55 100644 --- a/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/CreateFunction.kt +++ b/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/CreateFunction.kt @@ -68,10 +68,10 @@ suspend fun createNewFunction( description = "Created by the Lambda Kotlin API" handler = myHandler role = myRole - runtime = Runtime.Java8 + runtime = Runtime.Java17 } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> val functionResponse = awsLambda.createFunction(request) awsLambda.waitUntilFunctionActive { functionName = myFunctionName diff --git a/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/DeleteFunction.kt b/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/DeleteFunction.kt index e2d4d8f5e54..acef6a41cf9 100644 --- a/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/DeleteFunction.kt +++ b/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/DeleteFunction.kt @@ -42,7 +42,7 @@ suspend fun delLambdaFunction(myFunctionName: String) { functionName = myFunctionName } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> awsLambda.deleteFunction(request) println("$myFunctionName was deleted") } diff --git a/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/LambdaScenario.kt b/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/LambdaScenario.kt index ee938abceb8..c2d2393137a 100644 --- a/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/LambdaScenario.kt +++ b/kotlin/services/lambda/src/main/kotlin/com/kotlin/lambda/LambdaScenario.kt @@ -117,11 +117,11 @@ suspend fun createScFunction( description = "Created by the Lambda Kotlin API" handler = myHandler role = myRole - runtime = Runtime.Java8 + runtime = Runtime.Java17 } // Create a Lambda function using a waiter - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> val functionResponse = awsLambda.createFunction(request) awsLambda.waitUntilFunctionActive { functionName = myFunctionName @@ -136,7 +136,7 @@ suspend fun getFunction(functionNameVal: String) { functionName = functionNameVal } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> val response = awsLambda.getFunction(functionRequest) println("The runtime of this Lambda function is ${response.configuration?.runtime}") } @@ -148,7 +148,7 @@ suspend fun listFunctionsSc() { maxItems = 10 } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> val response = awsLambda.listFunctions(request) response.functions?.forEach { function -> println("The function name is ${function.functionName}") @@ -166,7 +166,7 @@ suspend fun invokeFunctionSc(functionNameVal: String) { logType = LogType.Tail } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> val res = awsLambda.invoke(request) println("The function payload is ${res.payload?.toString(Charsets.UTF_8)}") } @@ -185,7 +185,7 @@ suspend fun updateFunctionCode( s3Key = key } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> val response = awsLambda.updateFunctionCode(functionCodeRequest) awsLambda.waitUntilFunctionUpdated { functionName = functionNameVal @@ -202,10 +202,10 @@ suspend fun updateFunctionConfiguration( UpdateFunctionConfigurationRequest { functionName = functionNameVal handler = handlerVal - runtime = Runtime.Java11 + runtime = Runtime.Java17 } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> awsLambda.updateFunctionConfiguration(configurationRequest) } } @@ -216,7 +216,7 @@ suspend fun delFunction(myFunctionName: String) { functionName = myFunctionName } - LambdaClient { region = "us-west-2" }.use { awsLambda -> + LambdaClient { region = "us-east-1" }.use { awsLambda -> awsLambda.deleteFunction(request) println("$myFunctionName was deleted") } diff --git a/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt b/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt index cf17b447573..e873d41fd4d 100644 --- a/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt +++ b/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt @@ -125,7 +125,7 @@ class LambdaTest { // Update the AWS Lambda function code. println("*** Update the Lambda function code.") - updateFunctionCode(functionNameSc, updatedBucketName, s3Key) + updateFunctionCode(functionNameSc, s3BucketName, s3Key) // println("*** Invoke the function again after updating the code.") invokeFunctionSc(functionNameSc) diff --git a/kotlin/services/lex/build.gradle.kts b/kotlin/services/lex/build.gradle.kts index f34dced5b31..79c09d0ed5f 100644 --- a/kotlin/services/lex/build.gradle.kts +++ b/kotlin/services/lex/build.gradle.kts @@ -28,11 +28,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:lexruntimeservice:1.0.30") - implementation("aws.sdk.kotlin:secretsmanager:1.0.30") - implementation("aws.sdk.kotlin:lexmodelbuildingservice:1.0.30") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:lexruntimeservice") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.sdk.kotlin:lexmodelbuildingservice") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/lex/src/main/kotlin/com/kotlin/lex/GetBotStatus.kt b/kotlin/services/lex/src/main/kotlin/com/kotlin/lex/GetBotStatus.kt index 95d4bf7d207..e115f8e5124 100644 --- a/kotlin/services/lex/src/main/kotlin/com/kotlin/lex/GetBotStatus.kt +++ b/kotlin/services/lex/src/main/kotlin/com/kotlin/lex/GetBotStatus.kt @@ -54,7 +54,7 @@ suspend fun getStatus(botName: String?) { val response = lexClient.getBot(request) status = response.status.toString() println("The status is $status") - } while (status.compareTo("READY") != 0) + } while (status.compareTo("Ready") != 0) } } // snippet-end:[lex.kotlin.get_status.main] diff --git a/kotlin/services/mediaconvert/build.gradle.kts b/kotlin/services/mediaconvert/build.gradle.kts index 1e3b0b69ac1..8c01f96a3eb 100644 --- a/kotlin/services/mediaconvert/build.gradle.kts +++ b/kotlin/services/mediaconvert/build.gradle.kts @@ -28,10 +28,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:mediaconvert:1.0.30") - implementation("aws.sdk.kotlin:secretsmanager:1.0.30") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.0.30")) + implementation("aws.sdk.kotlin:mediaconvert") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt b/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt index b2c6a725bde..8c1cc99a7d9 100644 --- a/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt +++ b/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt @@ -1,6 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider +import aws.sdk.kotlin.services.mediaconvert.MediaConvertClient +import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson import com.kotlin.mediaconvert.createMediaJob diff --git a/kotlin/services/mediastore/build.gradle.kts b/kotlin/services/mediastore/build.gradle.kts index 6a2b24d1874..dc7ca982e4c 100644 --- a/kotlin/services/mediastore/build.gradle.kts +++ b/kotlin/services/mediastore/build.gradle.kts @@ -26,9 +26,10 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:mediastore:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:mediastore") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") } diff --git a/kotlin/services/opensearch/build.gradle.kts b/kotlin/services/opensearch/build.gradle.kts index a5ba54dcddd..74e58d61552 100644 --- a/kotlin/services/opensearch/build.gradle.kts +++ b/kotlin/services/opensearch/build.gradle.kts @@ -27,9 +27,10 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:opensearch:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:opensearch") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") diff --git a/kotlin/services/personalize/build.gradle.kts b/kotlin/services/personalize/build.gradle.kts index 8605242f23c..e6db9de0963 100644 --- a/kotlin/services/personalize/build.gradle.kts +++ b/kotlin/services/personalize/build.gradle.kts @@ -27,11 +27,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:personalize:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.sdk.kotlin:personalizeruntime:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:personalize") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.sdk.kotlin:personalizeruntime") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/pinpoint/build.gradle.kts b/kotlin/services/pinpoint/build.gradle.kts index 5b08a81f4d8..17e1e8e54b2 100644 --- a/kotlin/services/pinpoint/build.gradle.kts +++ b/kotlin/services/pinpoint/build.gradle.kts @@ -27,11 +27,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:pinpoint:1.2.28") - implementation("aws.sdk.kotlin:pinpointemail:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:pinpoint") + implementation("aws.sdk.kotlin:pinpointemail") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/polly/build.gradle.kts b/kotlin/services/polly/build.gradle.kts index 8564452b663..a0e7d838ebd 100644 --- a/kotlin/services/polly/build.gradle.kts +++ b/kotlin/services/polly/build.gradle.kts @@ -27,9 +27,10 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:polly:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:polly") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.googlecode.soundlibs:jlayer:1.0.1.4") diff --git a/kotlin/services/rds/build.gradle.kts b/kotlin/services/rds/build.gradle.kts index 2f966f33191..8fa7b076a7c 100644 --- a/kotlin/services/rds/build.gradle.kts +++ b/kotlin/services/rds/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:rds:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:rds") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/CreateDBInstance.kt b/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/CreateDBInstance.kt index ef51764c66f..68cf0dd3ff6 100644 --- a/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/CreateDBInstance.kt +++ b/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/CreateDBInstance.kt @@ -65,9 +65,9 @@ suspend fun createDatabaseInstance( allocatedStorage = 100 dbName = dbNamedbVal engine = "mysql" - dbInstanceClass = "db.m4.large" - engineVersion = "8.0" - storageType = "standard" + dbInstanceClass = "db.t3.micro" // Use a supported instance class + engineVersion = "8.0.39" // Use a supported engine version + storageType = "gp2" masterUsername = masterUsernameVal masterUserPassword = masterUserPasswordVal } diff --git a/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/RDSScenario.kt b/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/RDSScenario.kt index 9a2d63e85f5..3755ba17c2e 100644 --- a/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/RDSScenario.kt +++ b/kotlin/services/rds/src/main/kotlin/com/kotlin/rds/RDSScenario.kt @@ -306,9 +306,9 @@ suspend fun createDatabaseInstance( dbName = dbNameVal dbParameterGroupName = dbGroupNameVal engine = "mysql" - dbInstanceClass = "db.m4.large" - engineVersion = "8.0" - storageType = "standard" + dbInstanceClass = "db.t3.micro" + engineVersion = "8.0.35" + storageType = "gp2" masterUsername = masterUsernameVal masterUserPassword = masterUserPasswordVal } diff --git a/kotlin/services/redshift/README.md b/kotlin/services/redshift/README.md index 38ce245a996..7f87134bc62 100644 --- a/kotlin/services/redshift/README.md +++ b/kotlin/services/redshift/README.md @@ -36,7 +36,7 @@ Code excerpts that show you how to call individual service functions. - [CreateCluster](src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt#L56) - [DeleteCluster](src/main/kotlin/com/kotlin/redshift/DeleteCluster.kt#L38) - [DescribeClusters](src/main/kotlin/com/kotlin/redshift/DescribeClusters.kt#L22) -- [ModifyCluster](src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt#L112) +- [ModifyCluster](src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt#L113) @@ -83,4 +83,4 @@ in the `kotlin` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/kotlin/services/redshift/build.gradle.kts b/kotlin/services/redshift/build.gradle.kts index d3d4f953fad..0b8aa9aad99 100644 --- a/kotlin/services/redshift/build.gradle.kts +++ b/kotlin/services/redshift/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:redshift:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:redshift") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/redshift/src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt b/kotlin/services/redshift/src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt index 0804ae86cc3..248c2c691d9 100644 --- a/kotlin/services/redshift/src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt +++ b/kotlin/services/redshift/src/main/kotlin/com/kotlin/redshift/CreateAndModifyCluster.kt @@ -62,6 +62,7 @@ suspend fun createCluster( val clusterRequest = CreateClusterRequest { clusterIdentifier = clusterId + availabilityZone = "us-east-1a" masterUsername = masterUsernameVal masterUserPassword = masterUserPasswordVal nodeType = "ra3.4xlarge" diff --git a/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt b/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt index c5eb7d963ec..782b85e2126 100644 --- a/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt +++ b/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt @@ -3,14 +3,9 @@ import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson -import com.kotlin.redshift.User import com.kotlin.redshift.createCluster -import com.kotlin.redshift.deleteRedshiftCluster import com.kotlin.redshift.describeRedshiftClusters import com.kotlin.redshift.findReservedNodeOffer -import com.kotlin.redshift.listRedShiftEvents -import com.kotlin.redshift.modifyCluster -import com.kotlin.redshift.waitForClusterReady import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.DisplayName @@ -31,7 +26,8 @@ import java.util.Random class RedshiftKotlinTest { private var clusterId = "" private var eventSourceType = "" - private var secretName = "" + private var username = "" + private var password = "" @BeforeAll fun setup() = @@ -44,7 +40,8 @@ class RedshiftKotlinTest { val json: String = getSecretValues().toString() val values = gson.fromJson(json, SecretValues::class.java) clusterId = values.clusterId + randomNum - secretName = values.secretName.toString() + username = values.userName.toString() + password = values.password.toString() eventSourceType = values.eventSourceType.toString() // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. @@ -62,38 +59,12 @@ class RedshiftKotlinTest { @Order(1) fun createClusterTest() = runBlocking { - val gson = Gson() - val user = - gson.fromJson( - com.kotlin.redshift - .getSecretValues(secretName) - .toString(), - User::class.java, - ) - val username = user.username - val userPassword = user.password - createCluster(clusterId, username, userPassword) + createCluster(clusterId, username, password) println("Test 2 passed") } @Test @Order(2) - fun waitForClusterReadyTest() = - runBlocking { - waitForClusterReady(clusterId) - println("Test 3 passed") - } - - @Test - @Order(3) - fun modifyClusterReadyTest() = - runBlocking { - modifyCluster(clusterId) - println("Test 4 passed") - } - - @Test - @Order(4) fun describeClustersTest() = runBlocking { describeRedshiftClusters() @@ -101,29 +72,13 @@ class RedshiftKotlinTest { } @Test - @Order(5) + @Order(3) fun findReservedNodeOfferTest() = runBlocking { findReservedNodeOffer() println("Test 6 passed") } - @Test - @Order(6) - fun listEventsTest() = - runBlocking { - listRedShiftEvents(clusterId, eventSourceType) - println("Test 7 passed") - } - - @Test - @Order(7) - fun deleteClusterTest() = - runBlocking { - deleteRedshiftCluster(clusterId) - println("Test 8 passed") - } - suspend fun getSecretValues(): String? { val secretName = "test/red" val valueRequest = @@ -142,6 +97,7 @@ class RedshiftKotlinTest { internal inner class SecretValues { val clusterId: String? = null val eventSourceType: String? = null - val secretName: String? = null + val userName: String? = null + val password: String? = null } } diff --git a/kotlin/services/rekognition/build.gradle.kts b/kotlin/services/rekognition/build.gradle.kts index 621ba57b5af..0a39f0ec898 100644 --- a/kotlin/services/rekognition/build.gradle.kts +++ b/kotlin/services/rekognition/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:rekognition:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:rekognition") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectFaces.kt b/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectFaces.kt index 68073d965da..b730ceb46a3 100644 --- a/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectFaces.kt +++ b/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectFaces.kt @@ -106,7 +106,7 @@ suspend fun getFaceResults() { while (!finished) { response = rekClient.getFaceDetection(recognitionRequest) status = response.jobStatus.toString() - if (status.compareTo("SUCCEEDED") == 0) { + if (status.compareTo("Succeeded") == 0) { finished = true } else { println("$yy status is: $status") diff --git a/kotlin/services/route53/build.gradle.kts b/kotlin/services/route53/build.gradle.kts index 51f7ca26de0..36b47242bba 100644 --- a/kotlin/services/route53/build.gradle.kts +++ b/kotlin/services/route53/build.gradle.kts @@ -27,11 +27,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:route53:1.2.28") - implementation("aws.sdk.kotlin:route53domains:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:route53") + implementation("aws.sdk.kotlin:route53domains") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/s3/build.gradle.kts b/kotlin/services/s3/build.gradle.kts index 2de9daf041a..eee05ec6460 100644 --- a/kotlin/services/s3/build.gradle.kts +++ b/kotlin/services/s3/build.gradle.kts @@ -27,7 +27,7 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") -val kotlinSdkVersion = "1.0.41" +val kotlinSdkVersion = "1.3.112" val smithyKotlinVersion = "1.0.10" dependencies { implementation("aws.sdk.kotlin:s3:$kotlinSdkVersion") diff --git a/kotlin/services/sagemaker/build.gradle.kts b/kotlin/services/sagemaker/build.gradle.kts index b0a9cd8fc24..a4801da3ebe 100644 --- a/kotlin/services/sagemaker/build.gradle.kts +++ b/kotlin/services/sagemaker/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:sagemaker:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:sagemaker") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/secrets-manager/build.gradle.kts b/kotlin/services/secrets-manager/build.gradle.kts index 099e81082d1..6c04149f011 100644 --- a/kotlin/services/secrets-manager/build.gradle.kts +++ b/kotlin/services/secrets-manager/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/ses/build.gradle.kts b/kotlin/services/ses/build.gradle.kts index c56fba2b209..330b911026d 100644 --- a/kotlin/services/ses/build.gradle.kts +++ b/kotlin/services/ses/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:ses:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:ses") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") implementation("javax.mail:javax.mail-api:1.6.2") implementation("com.sun.mail:javax.mail:1.6.2") diff --git a/kotlin/services/ses/src/test/kotlin/SESTest.kt b/kotlin/services/ses/src/test/kotlin/SESTest.kt index f7e935dac7f..bdd46212395 100644 --- a/kotlin/services/ses/src/test/kotlin/SESTest.kt +++ b/kotlin/services/ses/src/test/kotlin/SESTest.kt @@ -7,7 +7,6 @@ import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson import com.kotlin.ses.listSESIdentities import com.kotlin.ses.send -import com.kotlin.ses.sendemailAttachment import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.DisplayName @@ -77,14 +76,6 @@ class SESTest { @Test @Order(2) - fun sendMessageAttTest() = - runBlocking { - sendemailAttachment(sender, recipient, subject, bodyText, bodyHTML, fileLocation) - println("Test 2 passed") - } - - @Test - @Order(3) fun listIdentitiesTest() = runBlocking { listSESIdentities() diff --git a/kotlin/services/sns/build.gradle.kts b/kotlin/services/sns/build.gradle.kts index 3e83260c257..83456a63f41 100644 --- a/kotlin/services/sns/build.gradle.kts +++ b/kotlin/services/sns/build.gradle.kts @@ -29,10 +29,11 @@ repositories { apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:sns:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:sns") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/sqs/build.gradle.kts b/kotlin/services/sqs/build.gradle.kts index 0ff57c2cb94..f850a57f2e8 100644 --- a/kotlin/services/sqs/build.gradle.kts +++ b/kotlin/services/sqs/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:sqs:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:sqs") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/stepfunctions/build.gradle.kts b/kotlin/services/stepfunctions/build.gradle.kts index ff7e17570b5..7d91e6a14b6 100644 --- a/kotlin/services/stepfunctions/build.gradle.kts +++ b/kotlin/services/stepfunctions/build.gradle.kts @@ -27,11 +27,12 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:sfn:1.2.28") - implementation("aws.sdk.kotlin:iam:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:sfn") + implementation("aws.sdk.kotlin:iam") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") implementation("com.googlecode.json-simple:json-simple:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") diff --git a/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/GetStream.kt b/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/GetStream.kt index 2ae11749b11..d365e9fb6c6 100644 --- a/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/GetStream.kt +++ b/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/GetStream.kt @@ -4,12 +4,13 @@ package com.kotlin.stepfunctions import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper +import java.io.FileInputStream import java.io.InputStream class GetStream { - suspend fun getStream(): String { + suspend fun getStream(location: String): String { // Get JSON to use for the state machine and place the activityArn value into it. - val input: InputStream = this::class.java.classLoader.getResourceAsStream("chat_sfn_state_machine.json") + val input: InputStream = FileInputStream(location) val mapper = ObjectMapper() val jsonNode: JsonNode = mapper.readValue(input, JsonNode::class.java) return mapper.writeValueAsString(jsonNode) diff --git a/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/StepFunctionsScenario.kt b/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/StepFunctionsScenario.kt index ed356ab8a01..c666510d832 100644 --- a/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/StepFunctionsScenario.kt +++ b/kotlin/services/stepfunctions/src/main/kotlin/com/kotlin/stepfunctions/StepFunctionsScenario.kt @@ -67,9 +67,10 @@ suspend fun main(args: Array) { roleName - The name of the IAM role to create for this state machine. activityName - The name of an activity to create. stateMachineName - The name of the state machine to create. + jsonFile - The location of the chat_sfn_state_machine.json file. You can located it in resources/sample_files. """ - if (args.size != 3) { + if (args.size != 4) { println(usage) exitProcess(0) } @@ -77,6 +78,7 @@ suspend fun main(args: Array) { val roleName = args[0] val activityName = args[1] val stateMachineName = args[2] + val jsonFile = args[3] val sc = Scanner(System.`in`) var action = false @@ -116,7 +118,7 @@ suspend fun main(args: Array) { // Get JSON to use for the state machine and place the activityArn value into it. val stream = GetStream() - val jsonString = stream.getStream() + val jsonString = stream.getStream(jsonFile) // Modify the Resource node. val objectMapper = ObjectMapper() @@ -258,14 +260,14 @@ suspend fun describeExe(executionArnVal: String?) { SfnClient { region = "us-east-1" }.use { sfnClient -> val response = sfnClient.describeExecution(executionRequest) status = response.status.toString() - if (status.compareTo("RUNNING") == 0) { + if (status.compareTo("Running") == 0) { println("The state machine is still running, let's wait for it to finish.") Thread.sleep(2000) - } else if (status.compareTo("SUCCEEDED") == 0) { + } else if (status.compareTo("Succeeded") == 0) { println("The Step Function workflow has succeeded") hasSucceeded = true } else { - println("The Status is neither running or succeeded") + println("The Status is $status") } } } diff --git a/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt b/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt index 961ac36994d..5eb7805d425 100644 --- a/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt +++ b/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt @@ -4,25 +4,8 @@ import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode import com.google.gson.Gson -import com.kotlin.stepfunctions.DASHES -import com.kotlin.stepfunctions.GetStream -import com.kotlin.stepfunctions.createActivity -import com.kotlin.stepfunctions.createIAMRole -import com.kotlin.stepfunctions.createMachine -import com.kotlin.stepfunctions.deleteActivity -import com.kotlin.stepfunctions.deleteMachine -import com.kotlin.stepfunctions.describeExe -import com.kotlin.stepfunctions.describeStateMachine -import com.kotlin.stepfunctions.getActivityTask -import com.kotlin.stepfunctions.listActivitesPagnator import com.kotlin.stepfunctions.listMachines -import com.kotlin.stepfunctions.listStatemachinesPagnator -import com.kotlin.stepfunctions.sendTaskSuccess -import com.kotlin.stepfunctions.startWorkflow import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.DisplayName @@ -32,7 +15,6 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder -import java.util.Scanner import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -69,115 +51,13 @@ class StepFunctionsKotlinTest { } @Test - @Order(2) + @Order(1) fun listStateMachines() = runBlocking { listMachines() println("Test 4 passed") } - @Test - @Order(2) - fun testMVP() = - runBlocking { - val sc = Scanner(System.`in`) - var action = false - - val polJSON = """{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "", - "Effect": "Allow", - "Principal": { - "Service": "states.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] - }""" - - println(DASHES) - println("List activities using a Paginator.") - listActivitesPagnator() - println("Create an activity.") - val activityArn = createActivity(activityNameSC) - println("The ARN of the Activity is $activityArn") - - println("List state machines using a paginator.") - listStatemachinesPagnator() - println(DASHES) - - // Get JSON to use for the state machine and place the activityArn value into it. - val stream = GetStream() - val jsonString = stream.getStream() - - // Modify the Resource node. - val objectMapper = ObjectMapper() - val root: JsonNode = objectMapper.readTree(jsonString) - (root.path("States").path("GetInput") as ObjectNode).put("Resource", activityArn) - - // Convert the modified Java object back to a JSON string. - val stateDefinition = objectMapper.writeValueAsString(root) - println(stateDefinition) - - println(DASHES) - println("Create a state machine.") - val roleARN = createIAMRole(roleNameSC, polJSON) - val stateMachineArn = createMachine(roleARN, stateMachineNameSC, stateDefinition) - println("The ARN of the state machine is $stateMachineArn") - println("The ARN of the state machine is") - println(DASHES) - - println(DASHES) - println("Describe the state machine.") - describeStateMachine(stateMachineArn) - println("What should ChatSFN call you?") - val userName = "foo" - println("Hello $userName") - println(DASHES) - - println(DASHES) - // The JSON to pass to the StartExecution call. - val executionJson = "{ \"name\" : \"$userName\" }" - println(executionJson) - println("Start execution of the state machine and interact with it.") - val runArn = startWorkflow(stateMachineArn, executionJson) - println("The ARN of the state machine execution is $runArn") - var myList: List - while (!action) { - myList = getActivityTask(activityArn) - println("ChatSFN: " + myList[1]) - println("$userName please specify a value.") - val myAction = "done" - action = true - val taskJson = "{ \"action\" : \"$myAction\" }" - println(taskJson) - sendTaskSuccess(myList[0], taskJson) - } - println(DASHES) - - println(DASHES) - println("Describe the execution.") - describeExe(runArn) - println(DASHES) - - println(DASHES) - println("Delete the activity.") - deleteActivity(activityArn) - println(DASHES) - - println(DASHES) - println("Delete the state machines.") - deleteMachine(stateMachineArn) - println(DASHES) - - println(DASHES) - println("The AWS Step Functions example scenario is complete.") - println(DASHES) - println("Test 4 passed") - } - private suspend fun getSecretValues(): String { val secretName = "test/stepfunctions" val valueRequest = diff --git a/kotlin/services/sts/build.gradle.kts b/kotlin/services/sts/build.gradle.kts index 91183182cc1..30221cf09d9 100644 --- a/kotlin/services/sts/build.gradle.kts +++ b/kotlin/services/sts/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:sts:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:sts") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/support/build.gradle.kts b/kotlin/services/support/build.gradle.kts index 10255a89550..8eb59dde0d3 100644 --- a/kotlin/services/support/build.gradle.kts +++ b/kotlin/services/support/build.gradle.kts @@ -27,12 +27,26 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:support:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") - testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:support") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") + implementation("com.google.code.gson:gson:2.10") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") } tasks.withType { kotlinOptions.jvmTarget = "17" } + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + + // Define the test source set + testClassesDirs += files("build/classes/kotlin/test") + classpath += files("build/classes/kotlin/main", "build/resources/main") +} diff --git a/kotlin/services/textract/build.gradle.kts b/kotlin/services/textract/build.gradle.kts index 224dbb0ef99..0013bc44e93 100644 --- a/kotlin/services/textract/build.gradle.kts +++ b/kotlin/services/textract/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:textract:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:textract") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/textract/src/main/kotlin/com/kotlin/textract/StartDocumentAnalysis.kt b/kotlin/services/textract/src/main/kotlin/com/kotlin/textract/StartDocumentAnalysis.kt index ee0a67506db..7a848fd9444 100644 --- a/kotlin/services/textract/src/main/kotlin/com/kotlin/textract/StartDocumentAnalysis.kt +++ b/kotlin/services/textract/src/main/kotlin/com/kotlin/textract/StartDocumentAnalysis.kt @@ -94,7 +94,7 @@ private suspend fun getJobResults( val response = textractClient.getDocumentAnalysis(analysisRequest) status = response.jobStatus.toString() - if (status.compareTo("SUCCEEDED") == 0) { + if (status.compareTo("Succeeded") == 0) { finished = true } else { println("$index status is: $status") diff --git a/kotlin/services/translate/build.gradle.kts b/kotlin/services/translate/build.gradle.kts index 2e03d8329ca..e5b87fa2464 100644 --- a/kotlin/services/translate/build.gradle.kts +++ b/kotlin/services/translate/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:translate:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:translate") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/xray/build.gradle.kts b/kotlin/services/xray/build.gradle.kts index 980ebfb26b9..04c0a3e12a7 100644 --- a/kotlin/services/xray/build.gradle.kts +++ b/kotlin/services/xray/build.gradle.kts @@ -27,10 +27,11 @@ repositories { } apply(plugin = "org.jlleitschuh.gradle.ktlint") dependencies { - implementation("aws.sdk.kotlin:xray:1.2.28") - implementation("aws.sdk.kotlin:secretsmanager:1.2.28") - implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.30.0") - implementation("aws.smithy.kotlin:http-client-engine-crt:0.30.0") + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:xray") + implementation("aws.sdk.kotlin:secretsmanager") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/kotlin/services/xray/src/test/kotlin/TranslateKotlinTest.kt b/kotlin/services/xray/src/test/kotlin/TranslateKotlinTest.kt deleted file mode 100644 index d2b3afc869e..00000000000 --- a/kotlin/services/xray/src/test/kotlin/TranslateKotlinTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider -import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient -import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest -import com.google.gson.Gson -import com.kotlin.translate.describeTranslationJob -import com.kotlin.translate.getTranslationJobs -import com.kotlin.translate.textTranslate -import com.kotlin.translate.translateDocuments -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.TestMethodOrder -import java.util.UUID - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestMethodOrder(OrderAnnotation::class) -class TranslateKotlinTest { - private var s3Uri = "" - private var s3UriOut = "" - private var jobName = "" - private var dataAccessRoleArn = "" - private var jobId = "" - - @BeforeAll - fun setup() = - runBlocking { - // Get the values to run these tests from AWS Secrets Manager. - val gson = Gson() - val json: String = getSecretValues() - val values = gson.fromJson(json, SecretValues::class.java) - s3Uri = values.s3Uri.toString() - s3UriOut = values.s3UriOut.toString() - jobName = values.jobName.toString() + UUID.randomUUID() - dataAccessRoleArn = values.dataAccessRoleArn.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - - // Populate the data members required for all tests. - s3Uri = prop.getProperty("s3Uri") - s3UriOut = prop.getProperty("s3UriOut") - jobName = prop.getProperty("jobName") - dataAccessRoleArn = prop.getProperty("dataAccessRoleArn") - */ - } - - @Test - @Order(1) - fun translateTextTest() = - runBlocking { - textTranslate() - println("Test 1 passed") - } - - @Test - @Order(2) - fun batchTranslationTest() = - runBlocking { - jobId = translateDocuments(s3Uri, s3UriOut, jobName, dataAccessRoleArn).toString() - Assertions.assertTrue(!jobId.isEmpty()) - println("Test 2 passed") - } - - @Test - @Order(3) - fun listTextTranslationJobsTest() = - runBlocking { - getTranslationJobs() - println("Test 3 passed") - } - - @Test - @Order(4) - fun describeTextTranslationJobTest() = - runBlocking { - describeTranslationJob(jobId) - println("Test 4 passed") - } - - private suspend fun getSecretValues(): String { - val secretName = "test/translate" - val valueRequest = - GetSecretValueRequest { - secretId = secretName - } - SecretsManagerClient { - region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() - }.use { secretClient -> - val valueResponse = secretClient.getSecretValue(valueRequest) - return valueResponse.secretString.toString() - } - } - - @Nested - @DisplayName("A class used to get test values from test/translate (an AWS Secrets Manager secret)") - internal class SecretValues { - val s3Uri: String? = null - val s3UriOut: String? = null - val jobName: String? = null - val dataAccessRoleArn: String? = null - } -} From f5673ede01b0d5862ca23e7cd20f56231a8212cc Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 09:26:55 -0500 Subject: [PATCH 009/144] added additional methods to the scenario --- .../entity/scenario/EntityResActions.java | 26 +++++++++++++++++++ .../entity/scenario/EntityResScenario.java | 1 + 2 files changed, 27 insertions(+) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 260a3bb482b..4a49c8c09af 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -21,12 +21,14 @@ import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; +import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; import software.amazon.awssdk.services.entityresolution.model.OutputSource; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -111,6 +113,30 @@ public static S3AsyncClient getS3AsyncClient() { return s3AsyncClient; } + // snippet-start:[entityres.java2_list_mappings.main] + /** + * Lists the schema mappings associated with the current AWS account. + * This method uses an asynchronous paginator to retrieve the schema mappings, + * and prints the name of each schema mapping to the console. + */ + public void ListSchemaMappings() { + ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() + .build(); + + ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); + + // Iterate through the pages of results + CompletableFuture future = paginator.subscribe(response -> { + response.schemaList().forEach(schemaMapping -> + System.out.println("Schema Mapping Name: " +schemaMapping.schemaName()) + ); + }); + + // Wait for the asynchronous operation to complete + future.join(); + } + // snippet-end:[entityres.java2_list_mappings.main] + // snippet-start:[entityres.java2_delete_matching_workflow.main] /** * Asynchronously deletes a workflow with the specified name. diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 83116dc1517..2df8618317a 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -191,6 +191,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println("6. List Schema Mappings."); + actions.ListSchemaMappings(); System.out.println(DASHES); System.out.println(DASHES); From 6ebf7434571d218c4f4fd1acde0a00f31c2fd67b Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 09:56:15 -0500 Subject: [PATCH 010/144] added additional methods to the scenario --- .../entity/scenario/EntityResScenario.java | 6 +++-- scenarios/basics/entity_resolution/README.md | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 2df8618317a..5446540f31c 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -29,8 +29,8 @@ public static void main(String[] args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = "MyMatchingWorkflow450"; - String schemaName = "schema450"; + String workflowName = "MyMatchingWorkflow451"; + String schemaName = "schema451"; // Use the AWS CDK to create this AWS resources. See the Readme file. String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; @@ -187,11 +187,13 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { System.err.println("Error retrieving schema mapping: " + ce.getCause().getMessage()); } + waitForInputToContinue(scanner); System.out.println(DASHES); System.out.println(DASHES); System.out.println("6. List Schema Mappings."); actions.ListSchemaMappings(); + waitForInputToContinue(scanner); System.out.println(DASHES); System.out.println(DASHES); diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 001e5a6a87b..4bae9fd6321 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -3,26 +3,24 @@ This AWS Entity Resolution basic scenario demonstrates how to interact with the ## Key Operations -1. **Create an AWS SiteWise Asset Model**: - - This step creates an AWS SiteWise Asset Model by invoking the `createAssetModel` method. +1. **Create an AWS Entity Resolution Schema Mapping**: + - This step creates an AWS Entity Resolution Schema Mapping by invoking the `createSchemaMapping` method. -2. **Create an AWS IoT SiteWise Asset**: - - This operation creates an AWS SiteWise asset. +2. **Create an AWS Entity Resolution Workflow**: + - This step creates an AWS Entity Resolution matching Workflow by invoking the `createMatchingWorkflow` method. -3. **Retrieve the property ID values**: - - To send data to an asset, we need to get the property ID values for the model properties. This scenario uses temperature and humidity properties. +3. **Start Matching Workflow**: + - This step starts the AWS Entity Resolution matching Workflow by invoking the `startMatchingJob` method. -4. **Send data to an AWS IoT SiteWise Asset**: - - This operation sends data to an IoT SiteWise Asset. +4. **Get Workflow Job Details**: + - This step gets workflow job details by `getMatchingJob` method. -5. **Retrieve the value of the IoT SiteWise Asset property**: - - This operation gets data from the asset. **Note** See the Eng spec for a full listing of operations. ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS IoT SiteWise service. The scenario creates this resource using a CloudFormation template. +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. ## Implementations @@ -30,10 +28,10 @@ This scenario example will be implemented in the following languages: - Java - Python -- JavaScript +- Kotlin ## Additional Reading -- [AWS IoT SiteWise Documentation](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/what-is-sitewise.html) +- [AWS Entity Resolution Documentation](https://docs.aws.amazon.com/entityresolution/latest/userguide/what-is-service.html) Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 From 566dc3806a6d9ed595432f6ae3df6b3f2bc8a89d Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 11:04:00 -0500 Subject: [PATCH 011/144] added a Readme for CDK for Entity Resolution --- .../cdk/entityresolution_resources/README.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 resources/cdk/entityresolution_resources/README.md diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md new file mode 100644 index 00000000000..098811142bd --- /dev/null +++ b/resources/cdk/entityresolution_resources/README.md @@ -0,0 +1,106 @@ +# AWS Entity Resolution resources + +## Overview + +Creates the following AWS resources for Amazon DynamoDB item tracker sample applications: + +* An AWS IAM role that has permissions required to run this Scenario. +* An AWS Glue table that provides the input data for the entity resolution matching workflow. +* An Amazon S3 input bucket that is used by the AWS Glue table. +* An Amazon S3 output bucket that is used by the matching workflow to store results of the matching workflow. + +## ⚠️ Important + +* Running this code might result in charges to your AWS account. +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + +## Deploy resources + +You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Interface +(AWS CLI) to deploy and destroy the resources for this example. + +### Deploy with the AWS CDK + +To deploy with the AWS CDK, you must install [Node.js](https://nodejs.org) and the +[AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). + +This example was built and tested with AWS CDK 2.33.0. + +Deploy AWS resources by running the following at a command prompt in this README's folder: + +``` +npm install +cdk deploy +``` + +The stack takes a few minutes to deploy. When it completes, it prints output like +the following: + +``` +Outputs: +doc-example-work-item-tracker-stack.TableName = doc-example-work-item-tracker +``` + +### Deploy with the AWS CLI + +To deploy with the AWS CLI, you must first install the +[AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). + +1. Deploy AWS resources by running the following at a command prompt in this README's folder: + + ``` + aws cloudformation create-stack --template-body file://setup.yaml --stack-name YOUR_STACK_NAME + ``` + + *Note:* The stack name must be unique within an AWS Region and AWS account. You can + specify up to 128 characters, and numbers and hyphens are allowed. + +2. The stack takes a few minutes to deploy. You can check status by running the following: + + ``` + aws cloudformation describe-stacks --stack-name YOUR_STACK_NAME + ``` + + When the stack is ready, it shows `StackStatus` of `CREATE_COMPLETE`. + +3. You can get the outputs from the stack by running the following: + + ``` + aws cloudformation describe-stacks --stack-name STACK_NAME --query Stacks[0].Outputs --output text + ``` + + This results in output like the following: + + ``` + TableName = doc-example-work-item-tracker + ``` + +## Destroy resources + +### Destroy with the AWS CDK + +You can use the AWS CDK to destroy the resources by running the following: + +``` +cdk destroy +``` + +### Destroy with the AWS CLI + +You can use the AWS CLI to destroy the resources by running the following: + +``` +aws cloudformation delete-stack --stack-name YOUR_STACK_NAME +``` + +## Additional resources + +* [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) +* [AWS CLI User Guide for Version 2](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) +* [AWS CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 From 795c3263669c31dd224d42a21c02901ba17f2f9d Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 11:22:22 -0500 Subject: [PATCH 012/144] added a Readme for CDK for Entity Resolution --- .../entity/scenario/EntityResScenario.java | 2 +- .../cdk/entityresolution_resources/README.md | 51 +++---------------- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 5446540f31c..d52769dd0dc 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -32,7 +32,7 @@ public static void main(String[] args) throws InterruptedException { String workflowName = "MyMatchingWorkflow451"; String schemaName = "schema451"; - // Use the AWS CDK to create this AWS resources. See the Readme file. + // Use the AWS CDK to create this AWS resources. See the Readme file located at . String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md index 098811142bd..3a1690adba3 100644 --- a/resources/cdk/entityresolution_resources/README.md +++ b/resources/cdk/entityresolution_resources/README.md @@ -21,15 +21,14 @@ You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Inte ### Deploy with the AWS CDK -To deploy with the AWS CDK, you must install [Node.js](https://nodejs.org) and the +To deploy with the AWS CDK, you must install [Java JDK](https://www.oracle.com/ca-en/java/technologies/downloads/) and the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). -This example was built and tested with AWS CDK 2.33.0. +This example was built and tested with AWS CDK 2.135.0. Deploy AWS resources by running the following at a command prompt in this README's folder: ``` -npm install cdk deploy ``` @@ -38,42 +37,12 @@ the following: ``` Outputs: -doc-example-work-item-tracker-stack.TableName = doc-example-work-item-tracker +EntityResolutionCdkStack.EntityResolutionArn = arn:aws:iam::XXXXX:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm +EntityResolutionCdkStack.GlueDataBucketName = glue-XXXXX3196d +EntityResolutionCdkStack.GlueTableArn = arn:aws:glue:us-east-1:XXXXX:table/entity_resolution_db/entity_resolution ``` -### Deploy with the AWS CLI - -To deploy with the AWS CLI, you must first install the -[AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). - -1. Deploy AWS resources by running the following at a command prompt in this README's folder: - - ``` - aws cloudformation create-stack --template-body file://setup.yaml --stack-name YOUR_STACK_NAME - ``` - - *Note:* The stack name must be unique within an AWS Region and AWS account. You can - specify up to 128 characters, and numbers and hyphens are allowed. - -2. The stack takes a few minutes to deploy. You can check status by running the following: - - ``` - aws cloudformation describe-stacks --stack-name YOUR_STACK_NAME - ``` - - When the stack is ready, it shows `StackStatus` of `CREATE_COMPLETE`. - -3. You can get the outputs from the stack by running the following: - - ``` - aws cloudformation describe-stacks --stack-name STACK_NAME --query Stacks[0].Outputs --output text - ``` - - This results in output like the following: - - ``` - TableName = doc-example-work-item-tracker - ``` +Note - Copy these AWS resources into your AWS Entity Resolution scenario. These values are required for the program to successfully run. ## Destroy resources @@ -85,14 +54,6 @@ You can use the AWS CDK to destroy the resources by running the following: cdk destroy ``` -### Destroy with the AWS CLI - -You can use the AWS CLI to destroy the resources by running the following: - -``` -aws cloudformation delete-stack --stack-name YOUR_STACK_NAME -``` - ## Additional resources * [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) From 9d32734e06ae9fb9586ffd411a9b547a147507f9 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 11:32:07 -0500 Subject: [PATCH 013/144] added a source files for CDK --- .../entity/scenario/EntityResScenario.java | 3 +- .../cdk/entityresolution_resources/README.md | 4 +- .../cdk/entityresolution_resources/pom.xml | 60 +++++++++ .../com/myorg/EntityResolutionCdkApp.java | 45 +++++++ .../com/myorg/EntityResolutionCdkStack.java | 117 ++++++++++++++++++ .../com/myorg/EntityResolutionCdkTest.java | 26 ++++ 6 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 resources/cdk/entityresolution_resources/pom.xml create mode 100644 resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java create mode 100644 resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java create mode 100644 resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index d52769dd0dc..3166e662a0c 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -32,7 +32,8 @@ public static void main(String[] args) throws InterruptedException { String workflowName = "MyMatchingWorkflow451"; String schemaName = "schema451"; - // Use the AWS CDK to create this AWS resources. See the Readme file located at . + // Use the AWS CDK to create these AWS resources. + // See the Readme file located at resources/cdk/entityresolution_resources. String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md index 3a1690adba3..262d244b901 100644 --- a/resources/cdk/entityresolution_resources/README.md +++ b/resources/cdk/entityresolution_resources/README.md @@ -2,7 +2,7 @@ ## Overview -Creates the following AWS resources for Amazon DynamoDB item tracker sample applications: +Creates the following AWS resources for the AWS Entity Resolution scenario: * An AWS IAM role that has permissions required to run this Scenario. * An AWS Glue table that provides the input data for the entity resolution matching workflow. @@ -21,7 +21,7 @@ You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Inte ### Deploy with the AWS CDK -To deploy with the AWS CDK, you must install [Java JDK](https://www.oracle.com/ca-en/java/technologies/downloads/) and the +To deploy with the AWS CDK, you must install [Java JDK 17](https://www.oracle.com/ca-en/java/technologies/downloads/) and the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). This example was built and tested with AWS CDK 2.135.0. diff --git a/resources/cdk/entityresolution_resources/pom.xml b/resources/cdk/entityresolution_resources/pom.xml new file mode 100644 index 00000000000..56581afc3e7 --- /dev/null +++ b/resources/cdk/entityresolution_resources/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.myorg + entity_resolution_cdk + 0.1 + + + UTF-8 + 2.135.0 + [10.0.0,11.0.0) + 5.7.1 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.myorg.EntityResolutionCdkApp + + + + + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + + software.constructs + constructs + ${constructs.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java new file mode 100644 index 00000000000..e7428abe1da --- /dev/null +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.Environment; +import software.amazon.awscdk.StackProps; + +import java.util.Arrays; + +public class EntityResolutionCdkApp { + public static void main(final String[] args) { + App app = new App(); + + new EntityResolutionCdkStack(app, "EntityResolutionCdkStack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + .env(Environment.builder() + .account(System.getenv("CDK_DEFAULT_ACCOUNT")) + .region(System.getenv("CDK_DEFAULT_REGION")) + .build()) + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + .env(Environment.builder() + .account("123456789012") + .region("us-east-1") + .build()) + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + .build()); + + app.synth(); + } +} + diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java new file mode 100644 index 00000000000..4aecc53409a --- /dev/null +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -0,0 +1,117 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.myorg; + +import software.amazon.awscdk.*; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awscdk.services.s3.*; +import software.amazon.awscdk.services.glue.*; +import software.constructs.Construct; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class EntityResolutionCdkStack extends Stack { + public EntityResolutionCdkStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public EntityResolutionCdkStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + // 1. Create an S3 bucket for the Glue Data Table + String uniqueId = UUID.randomUUID().toString().replace("-", ""); // Remove dashes to ensure compatibility + Bucket glueDataBucket = Bucket.Builder.create(this, "GlueDataBucket") + .bucketName("glue-" + uniqueId) + .versioned(true) + .build(); + + // 2. Create a Glue database + CfnDatabase glueDatabase = CfnDatabase.Builder.create(this, "GlueDatabase") + .catalogId(this.getAccount()) + .databaseInput(CfnDatabase.DatabaseInputProperty.builder() + .name("entity_resolution_db") + .build()) + .build(); + + // 3. Create a Glue table referencing the S3 bucket + CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") + .catalogId(this.getAccount()) + .databaseName(glueDatabase.getRef()) // Ensure Glue Table references the database correctly + .tableInput(CfnTable.TableInputProperty.builder() + .name("entity_resolution") // Fixed table name reference + .tableType("EXTERNAL_TABLE") + .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() + .columns(List.of( + CfnTable.ColumnProperty.builder().name("id").type("string").build(), // Fixed: id is a string, + CfnTable.ColumnProperty.builder().name("name").type("string").build(), + CfnTable.ColumnProperty.builder().name("email").type("string").build() + )) + .location("s3://" + glueDataBucket.getBucketName() + "/data/") // Append subpath for data + .inputFormat("org.apache.hadoop.mapred.TextInputFormat") + .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") + .serdeInfo(CfnTable.SerdeInfoProperty.builder() + .serializationLibrary("org.openx.data.jsonserde.JsonSerDe") // Set JSON SerDe + .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON + .build()) + .build()) + .build()) + .build(); + + // Ensure Glue Table is created after the Database + glueTable.addDependency(glueDatabase); + + // 4. Create an IAM Role for AWS Entity Resolution + Role entityResolutionRole = Role.Builder.create(this, "EntityResolutionRole") + .assumedBy(new ServicePrincipal("entityresolution.amazonaws.com")) // AWS Entity Resolution assumes this role + .managedPolicies(List.of( + ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"), + ManagedPolicy.fromAwsManagedPolicyName("AWSEntityResolutionConsoleFullAccess"), + ManagedPolicy.fromAwsManagedPolicyName("AWSGlueConsoleFullAccess"), + ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSGlueServiceRole") + )) + .build(); + + // Add custom permissions for Entity Resolution + entityResolutionRole.addToPolicy(PolicyStatement.Builder.create() + .actions(List.of( + "entityresolution:StartMatchingWorkflow", + "entityresolution:GetMatchingWorkflow" + )) + .resources(List.of("*")) // Adjust permissions if needed + .build()); + + // 5. Create an S3 bucket for output data + Bucket outputBucket = Bucket.Builder.create(this, "OutputBucket") + .bucketName("entity-resolution-output-" + id.toLowerCase()) + .versioned(true) + .build(); + + // 6. Output the Role ARN + new CfnOutput(this, "EntityResolutionArn", CfnOutputProps.builder() + .value(entityResolutionRole.getRoleArn()) + .description("The ARN of the Glue Role") + .build()); + + // 7. Construct and output the Glue Table ARN + String glueTableArn = String.format("arn:aws:glue:%s:%s:table/%s/%s", + this.getRegion(), // Region where the stack is deployed + this.getAccount(), // AWS account ID + glueDatabase.getRef(), // Glue database name (resolved reference) + "entity_resolution" // Corrected table name + ); + + new CfnOutput(this, "GlueTableArn", CfnOutputProps.builder() + .value(glueTableArn) + .description("The ARN of the Glue Table") + .build()); + + // 8. Output the name of the Glue Data Bucket + new CfnOutput(this, "GlueDataBucketName", CfnOutputProps.builder() + .value(glueDataBucket.getBucketName()) // Outputs the bucket name + .description("The name of the Glue Data Bucket") + .build()); + } +} diff --git a/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java b/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java new file mode 100644 index 00000000000..ac832c96bdb --- /dev/null +++ b/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java @@ -0,0 +1,26 @@ +// package com.myorg; + +// import software.amazon.awscdk.App; +// import software.amazon.awscdk.assertions.Template; +// import java.io.IOException; + +// import java.util.HashMap; + +// import org.junit.jupiter.api.Test; + +// example test. To run these tests, uncomment this file, along with the +// example resource in java/src/main/java/com/myorg/EntityResolutionCdkStack.java +// public class EntityResolutionCdkTest { + +// @Test +// public void testStack() throws IOException { +// App app = new App(); +// EntityResolutionCdkStack stack = new EntityResolutionCdkStack(app, "test"); + +// Template template = Template.fromStack(stack); + +// template.hasResourceProperties("AWS::SQS::Queue", new HashMap() {{ +// put("VisibilityTimeout", 300); +// }}); +// } +// } From e2e55013d3d8c2b4969bcf2bcdf08c2466fa5d00 Mon Sep 17 00:00:00 2001 From: Chris Rees <34663864+AWSChris@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:10:34 -0800 Subject: [PATCH 014/144] Python: Use InvokeFlow for a turn by turn conversation. (#7210) --- .../bedrock-agent-runtime_metadata.yaml | 21 ++ .../bedrock-agent-runtime/README.md | 27 ++- .../flows/flow-conversation.py | 182 ++++++++++++++++++ .../test/test_flow_conversation.py | 31 +++ 4 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 python/example_code/bedrock-agent-runtime/flows/flow-conversation.py create mode 100644 python/example_code/bedrock-agent-runtime/test/test_flow_conversation.py diff --git a/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml index 9d2e42ce39b..c486a46b624 100644 --- a/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml @@ -32,3 +32,24 @@ bedrock-agent-runtime_InvokeFlow: - javascriptv3/example_code/bedrock-agent-runtime/actions/invoke-flow.js services: bedrock-agent-runtime: {InvokeFlow} + +bedrock-agent-runtime_Scenario_ConverseWithFlow: + title: Converse with an &BRlong; flow + synopsis: use InvokeFlow to converse with an &BRlong; flow that includes an agent node. + category: Basics + guide_topic: + title: Converse with an &BRlong; flow + url: bedrock/latest/userguide/flows-multi-turn-invocation.html + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-agent-runtime + sdkguide: + excerpts: + - description: + snippet_tags: + - python.example_code.bedrock-agent-runtime.flow_conversation.complete + + services: + bedrock-agent-runtime: {InvokeFlow} diff --git a/python/example_code/bedrock-agent-runtime/README.md b/python/example_code/bedrock-agent-runtime/README.md index b6bc29f2684..f3c625a3bf4 100644 --- a/python/example_code/bedrock-agent-runtime/README.md +++ b/python/example_code/bedrock-agent-runtime/README.md @@ -34,6 +34,13 @@ python -m pip install -r requirements.txt +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](flows/flow-conversation.py) + + ### Single actions Code excerpts that show you how to call individual service functions. @@ -53,6 +60,24 @@ Code excerpts that show you how to call individual service functions. +#### Learn the basics + +This example shows you how to use InvokeFlow to converse with an Amazon Bedrock flow that includes an agent node. + + + + + +Start the example by running the following at a command prompt: + +``` +python flows/flow-conversation.py +``` + + + + + ### Tests @@ -80,4 +105,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/bedrock-agent-runtime/flows/flow-conversation.py b/python/example_code/bedrock-agent-runtime/flows/flow-conversation.py new file mode 100644 index 00000000000..b5fccd1c2e5 --- /dev/null +++ b/python/example_code/bedrock-agent-runtime/flows/flow-conversation.py @@ -0,0 +1,182 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[python.example_code.bedrock-agent-runtime.flow_conversation.complete] + + +""" +Shows how to run an Amazon Bedrock flow with InvokeFlow and handle muli-turn interaction +for a single conversation. +For more information, see https://docs.aws.amazon.com/bedrock/latest/userguide/flows-multi-turn-invocation.html. + +""" +import logging +import boto3 +import botocore + +import botocore.exceptions + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def invoke_flow(client, flow_id, flow_alias_id, input_data, execution_id): + """ + Invoke an Amazon Bedrock flow and handle the response stream. + + Args: + client: Boto3 client for Amazon Bedrock agent runtime. + flow_id: The ID of the flow to invoke. + flow_alias_id: The alias ID of the flow. + input_data: Input data for the flow. + execution_id: Execution ID for continuing a flow. Use the value None on first run. + + Returns: + Dict containing flow_complete status, input_required info, and execution_id + """ + + response = None + request_params = None + + if execution_id is None: + # Don't pass execution ID for first run. + request_params = { + "flowIdentifier": flow_id, + "flowAliasIdentifier": flow_alias_id, + "inputs": [input_data], + "enableTrace": True + } + else: + request_params = { + "flowIdentifier": flow_id, + "flowAliasIdentifier": flow_alias_id, + "executionId": execution_id, + "inputs": [input_data], + "enableTrace": True + } + + response = client.invoke_flow(**request_params) + + if "executionId" not in request_params: + execution_id = response['executionId'] + + input_required = None + flow_status = "" + + # Process the streaming response + for event in response['responseStream']: + + # Check if flow is complete. + if 'flowCompletionEvent' in event: + flow_status = event['flowCompletionEvent']['completionReason'] + + # Check if more input us needed from user. + elif 'flowMultiTurnInputRequestEvent' in event: + input_required = event + + # Print the model output. + elif 'flowOutputEvent' in event: + print(event['flowOutputEvent']['content']['document']) + + # Log trace events. + elif 'flowTraceEvent' in event: + logger.info("Flow trace: %s", event['flowTraceEvent']) + + return { + "flow_status": flow_status, + "input_required": input_required, + "execution_id": execution_id + } + + +def converse_with_flow(bedrock_agent_client, flow_id, flow_alias_id): + """ + Run a conversation with the supplied flow. + + Args: + bedrock_agent_client: Boto3 client for Amazon Bedrock agent runtime. + flow_id: The ID of the flow to run. + flow_alias_id: The alias ID of the flow. + + """ + + flow_execution_id = None + finished = False + + # Get the intial prompt from the user. + user_input = input("Enter input: ") + + # Use prompt to create input data. + flow_input_data = { + "content": { + "document": user_input + }, + "nodeName": "FlowInputNode", + "nodeOutputName": "document" + } + + try: + while not finished: + # Invoke the flow until successfully finished. + + result = invoke_flow( + bedrock_agent_client, flow_id, flow_alias_id, flow_input_data, flow_execution_id) + + status = result['flow_status'] + flow_execution_id = result['execution_id'] + more_input = result['input_required'] + if status == "INPUT_REQUIRED": + # The flow needs more information from the user. + logger.info("The flow %s requires more input", flow_id) + user_input = input( + more_input['flowMultiTurnInputRequestEvent']['content']['document'] + ": ") + flow_input_data = { + "content": { + "document": user_input + }, + "nodeName": more_input['flowMultiTurnInputRequestEvent']['nodeName'], + "nodeInputName": "agentInputText" + + } + elif status == "SUCCESS": + # The flow completed successfully. + finished = True + logger.info("The flow %s successfully completed.", flow_id) + + except botocore.exceptions.ClientError as e: + print(f"Client error: {str(e)}") + logger.error("Client error: %s", {str(e)}) + + except Exception as e: + print(f"An error occurred: {str(e)}") + logger.error("An error occurred: %s", {str(e)}) + logger.error("Error type: %s", {type(e)}) + + +def main(): + """ + Main entry point for the script. + """ + + # Replace these with your actual flow ID and flow alias ID. + FLOW_ID = 'YOUR_FLOW_ID' + FLOW_ALIAS_ID = 'YOUR_FLOW_ALIAS_ID' + + logger.info("Starting conversation with FLOW: %s ID: %s", + FLOW_ID, FLOW_ALIAS_ID) + + # Get the Bedrock agent runtime client. + session = boto3.Session(profile_name='default') + bedrock_agent_client = session.client('bedrock-agent-runtime') + + # Start the conversation. + converse_with_flow(bedrock_agent_client, FLOW_ID, FLOW_ALIAS_ID) + + logger.info("Conversation with FLOW: %s ID: %s finished", + FLOW_ID, FLOW_ALIAS_ID) + + +if __name__ == "__main__": + main() + + # snippet-end:[python.example_code.bedrock-agent-runtime.flow_conversation.complete] diff --git a/python/example_code/bedrock-agent-runtime/test/test_flow_conversation.py b/python/example_code/bedrock-agent-runtime/test/test_flow_conversation.py new file mode 100644 index 00000000000..fd619675009 --- /dev/null +++ b/python/example_code/bedrock-agent-runtime/test/test_flow_conversation.py @@ -0,0 +1,31 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import subprocess +import sys + +files_under_test = [ + "flows/flow-conversation.py" +] + +@pytest.mark.integ +@pytest.mark.parametrize("file", files_under_test) +def test_flow_conversation(file): + # Simulate user input - each string represents one input() call + # If you're using the docs at https://docs.aws.amazon.com/bedrock/latest/userguide/flows-multi-turn-invocation.html, + # "Create a playlist\n 3\n pop, castles\n" should work with Antropic Haiku. + test_input = "Hello\n" + + result = subprocess.run( + [sys.executable, file], + input=test_input, + capture_output=True, + text=True, + ) + + print(f"STDOUT: {result.stdout}") # For debugging + print(f"STDERR: {result.stderr}") # For debugging + + assert result.stdout != "" + assert result.returncode == 0 From 2dedd04fba46511ac6bf5f42b32f318c7513ef76 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 3 Feb 2025 12:20:21 -0500 Subject: [PATCH 015/144] added tests --- .../src/test/java/EntityResTests.java | 150 ++++++++++++++++++ scenarios/basics/entity_resolution/README.md | 2 +- 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 javav2/example_code/entityresolution/src/test/java/EntityResTests.java diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java new file mode 100644 index 00000000000..113d9a44bbc --- /dev/null +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -0,0 +1,150 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + + +import com.example.entity.scenario.EntityResActions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class EntityResTests { + + private static String workflowName = ""; + private static String schemaName = ""; + + private static String roleARN = ""; + private static String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + private static String outputBucket = ""; + private static String inputGlueTableArn = ""; + + private static String mappingARN = ""; + + private static String jobId = ""; + + private static String workflowArn =""; + private static EntityResActions actions = new EntityResActions(); + @BeforeAll + public static void setUp() { + workflowName = "MyMatchingWorkflow456"; + schemaName = "schema456"; + roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; + dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; + inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + + String json = """ + [ + { + "id": "1", + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + { + "id": "2", + "name": "Bob Smith", + "email": "bob.smith@example.com" + }, + { + "id": "3", + "name": "Charlie Black", + "email": "charlie.black@example.com" + } + ] + """; + if (!actions.doesObjectExist(dataS3bucket)) { + actions.uploadLocalFileAsync(dataS3bucket, json); + } else { + System.out.println("The JSON exists in " + dataS3bucket); + } + } + + @Test + @Tag("IntegrationTest") + @Order(1) + public void testCreateMapping() { + assertDoesNotThrow(() -> { + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); + mappingARN = response.schemaArn(); + assertNotNull(mappingARN); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(2) + public void testCreateMappingWorkflow() { + assertDoesNotThrow(() -> { + workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + assertNotNull(workflowArn); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(3) + public void testStartWorkflow() { + assertDoesNotThrow(() -> { + jobId = actions.startMatchingJobAsync(workflowName).join(); + assertNotNull(workflowArn); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(4) + public void testGetJobDetails() { + assertDoesNotThrow(() -> { + actions.getMatchingJobAsync(jobId, workflowName).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(5) + public void testtSchemaMappingDetails() { + assertDoesNotThrow(() -> { + actions.getSchemaMappingAsync(schemaName).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(6) + public void testListSchemaMappings() { + assertDoesNotThrow(() -> { + actions.ListSchemaMappings(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(7) + public void testLTagResources() { + assertDoesNotThrow(() -> { + actions.tagEntityResource(mappingARN).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(8) + public void testLDeleteMapping() { + assertDoesNotThrow(() -> { + System.out.println("Wait 30 mins for the workflow to complete"); + Thread.sleep(1800000); + actions.deleteMatchingWorkflowAsync(workflowName).join(); + }); + } + + +} diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 4bae9fd6321..6099ece90f5 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -20,7 +20,7 @@ This AWS Entity Resolution basic scenario demonstrates how to interact with the ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. See the Readme file at resources/cdk/entityresolution_resources. ## Implementations From bf6f886d05338d7c324c09e549527fbea298c612 Mon Sep 17 00:00:00 2001 From: Scott Macdonald <57190223+scmacdon@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:44:40 -0500 Subject: [PATCH 016/144] Kotlin Removed env variable provider (#7223) removed env variable provider --- kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt b/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt index f5908bf0235..e1e7315c3ca 100644 --- a/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt +++ b/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.apigateway.ApiGatewayClient import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest @@ -96,7 +95,6 @@ class APIGatewayTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() From 7e25992db3366282a2095535823d5440bd2fa565 Mon Sep 17 00:00:00 2001 From: Scott Macdonald <57190223+scmacdon@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:47:03 -0500 Subject: [PATCH 017/144] Kotlin Added logging functionality to tests (#7225) --- kotlin/services/apigateway/build.gradle.kts | 2 ++ .../src/test/kotlin/APIGatewayTest.kt | 27 +++++------------ .../apigateway/src/test/resources/logback.xml | 11 +++++++ kotlin/services/appsync/build.gradle.kts | 2 ++ .../appsync/src/test/kotlin/AppSyncTest.kt | 30 +++++++------------ .../appsync/src/test/resources/logback.xml | 11 +++++++ 6 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 kotlin/services/apigateway/src/test/resources/logback.xml create mode 100644 kotlin/services/appsync/src/test/resources/logback.xml diff --git a/kotlin/services/apigateway/build.gradle.kts b/kotlin/services/apigateway/build.gradle.kts index 26b3b97dc40..cfb8b0a2359 100644 --- a/kotlin/services/apigateway/build.gradle.kts +++ b/kotlin/services/apigateway/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") implementation("com.google.code.gson:gson:2.10") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt b/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt index e1e7315c3ca..c3542890d3b 100644 --- a/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt +++ b/kotlin/services/apigateway/src/test/kotlin/APIGatewayTest.kt @@ -18,11 +18,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class APIGatewayTest { + private val logger: Logger = LoggerFactory.getLogger(APIGatewayTest::class.java) lateinit var apiGatewayClient: ApiGatewayClient private var restApiId = "" private var httpMethod = "" @@ -33,7 +36,6 @@ class APIGatewayTest { @BeforeAll fun setup() = runBlocking { apiGatewayClient = ApiGatewayClient { region = "us-east-1" } - // Get values from AWS Secrets Manager. val random = Random() val randomNum = random.nextInt(10000 - 1 + 1) + 1 val gson = Gson() @@ -43,49 +45,34 @@ class APIGatewayTest { httpMethod = values.httpMethod.toString() restApiName = values.restApiName.toString() + randomNum stageName = values.stageName.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // load the properties file. - prop.load(input) - - // Populate the data members required for all tests - restApiId = prop.getProperty("restApiId") - resourceId = prop.getProperty("resourceId") - httpMethod = prop.getProperty("httpMethod") - restApiName = prop.getProperty("restApiName") - stageName = prop.getProperty("stageName") - */ } @Test @Order(1) fun createRestApiTest() = runBlocking { newApiId = createAPI(restApiId).toString() - println("Test 2 passed") + logger.info("Test 1 passed") } @Test @Order(2) fun getDeploymentsTest() = runBlocking { getAllDeployments(newApiId) - println("Test 4 passed") + logger.info("Test 2 passed") } @Test @Order(3) fun getAllStagesTest() = runBlocking { getAllStages(newApiId) - println("Test 5 passed") + logger.info("Test 3 passed") } @Test @Order(4) fun deleteRestApi() = runBlocking { deleteAPI(newApiId) - println("Test 6 passed") + logger.info("Test 4 passed") } private suspend fun getSecretValues(): String { diff --git a/kotlin/services/apigateway/src/test/resources/logback.xml b/kotlin/services/apigateway/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/apigateway/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/appsync/build.gradle.kts b/kotlin/services/appsync/build.gradle.kts index 5b8b1111486..dbb9265f0d8 100644 --- a/kotlin/services/appsync/build.gradle.kts +++ b/kotlin/services/appsync/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { implementation("com.googlecode.json-simple:json-simple:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/appsync/src/test/kotlin/AppSyncTest.kt b/kotlin/services/appsync/src/test/kotlin/AppSyncTest.kt index a4c4e0344af..ede5e243617 100644 --- a/kotlin/services/appsync/src/test/kotlin/AppSyncTest.kt +++ b/kotlin/services/appsync/src/test/kotlin/AppSyncTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.example.appsync.createDS @@ -21,10 +20,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class AppSyncTest { + private val logger: Logger = LoggerFactory.getLogger(AppSyncTest::class.java) private var apiId = "" private var dsName = "" private var dsRole = "" @@ -42,17 +44,6 @@ class AppSyncTest { dsName = values.dsName.toString() dsRole = values.dsRole.toString() tableName = values.tableName.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - apiId = prop.getProperty("apiId") - dsName = prop.getProperty("dsName") - dsRole = prop.getProperty("dsRole") - tableName = prop.getProperty("tableName") - */ } @Test @@ -61,7 +52,7 @@ class AppSyncTest { runBlocking { keyId = createKey(apiId).toString() assertTrue(!keyId.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -72,7 +63,7 @@ class AppSyncTest { if (dsARN != null) { assertTrue(dsARN.isNotEmpty()) } - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -80,7 +71,7 @@ class AppSyncTest { fun getDataSource() = runBlocking { getDS(apiId, dsName) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -88,7 +79,7 @@ class AppSyncTest { fun listGraphqlApis() = runBlocking { getKeys(apiId) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -96,7 +87,7 @@ class AppSyncTest { fun listApiKeys() = runBlocking { getKeys(apiId) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -104,7 +95,7 @@ class AppSyncTest { fun deleteDataSource() = runBlocking { deleteDS(apiId, dsName) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -112,7 +103,7 @@ class AppSyncTest { fun deleteApiKey() = runBlocking { deleteKey(keyId, apiId) - println("Test 7 passed") + logger.info("Test 7 passed") } private suspend fun getSecretValues(): String { @@ -123,7 +114,6 @@ class AppSyncTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/appsync/src/test/resources/logback.xml b/kotlin/services/appsync/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/appsync/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From cc62d019bf0e3baaf95548ade8e2ee3c8c6790fc Mon Sep 17 00:00:00 2001 From: Scott Macdonald <57190223+scmacdon@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:59:19 -0500 Subject: [PATCH 018/144] Kotlin Updated a Provider for tests and added logging functionality (#7226) --- kotlin/services/athena/build.gradle.kts | 2 + .../athena/src/test/kotlin/AthenaTest.kt | 26 ++-- .../athena/src/test/resources/logback.xml | 11 ++ .../services/cloudformation/build.gradle.kts | 2 + .../src/test/kotlin/CloudFormationTest.kt | 26 +--- .../src/test/resources/logback.xml | 11 ++ kotlin/services/cloudtrail/build.gradle.kts | 2 + .../src/test/kotlin/CloudtrailKotlinTest.kt | 30 ++--- .../cloudtrail/src/test/resources/logback.xml | 11 ++ kotlin/services/cloudwatch/build.gradle.kts | 2 + .../src/test/kotlin/CloudWatchTest.kt | 58 +++----- .../cloudwatch/src/test/resources/logback.xml | 11 ++ kotlin/services/codepipeline/build.gradle.kts | 2 + .../src/test/kotlin/PipelineServiceTest.kt | 17 +-- .../src/test/resources/config.properties | 4 - .../src/test/resources/logback.xml | 11 ++ kotlin/services/cognito/build.gradle.kts | 2 + .../src/test/kotlin/CognitoKotlinTest.kt | 55 ++------ .../cognito/src/test/resources/logback.xml | 11 ++ kotlin/services/comprehend/build.gradle.kts | 2 + .../src/test/java/ComprehendKotlinTest.kt | 30 ++--- .../comprehend/src/test/resources/logback.xml | 11 ++ kotlin/services/dynamodb/build.gradle.kts | 2 + .../dynamodb/src/test/kotlin/DynamoDB.kt | 49 +++---- .../dynamodb/src/test/resources/logback.xml | 11 ++ kotlin/services/ec2/build.gradle.kts | 2 + .../services/ec2/src/test/kotlin/EC2Test.kt | 65 +++------ .../ec2/src/test/resources/logback.xml | 11 ++ kotlin/services/ecr/build.gradle.kts | 3 +- .../services/ecr/src/test/kotlin/ECRTest.kt | 24 +--- .../ecr/src/test/resources/logback.xml | 11 ++ kotlin/services/ecs/build.gradle.kts | 2 + .../services/ecs/src/test/kotlin/ESCTest.kt | 34 ++--- .../ecs/src/test/resources/logback.xml | 11 ++ .../elasticbeanstalk/build.gradle.kts | 2 + .../src/test/kotlin/ElasticBeanstalkTest.kt | 17 ++- .../src/test/resources/logback.xml | 11 ++ kotlin/services/emr/build.gradle.kts | 2 + .../services/emr/src/test/kotlin/EMRTest.kt | 33 +---- .../emr/src/test/resources/logback.xml | 11 ++ kotlin/services/eventbridge/build.gradle.kts | 2 + .../src/test/kotlin/EventBridgeKotlinTest.kt | 5 +- .../src/test/resources/logback.xml | 11 ++ kotlin/services/firehose/build.gradle.kts | 2 + .../firehose/src/test/kotlin/FirehoseTest.kt | 39 ++---- .../firehose/src/test/resources/logback.xml | 11 ++ kotlin/services/forecast/build.gradle.kts | 2 + .../src/test/kotlin/ForecastKotlinTest.kt | 32 ++--- .../forecast/src/test/resources/logback.xml | 11 ++ kotlin/services/glue/build.gradle.kts | 2 + .../services/glue/src/test/kotlin/GlueTest.kt | 30 +---- .../glue/src/test/resources/logback.xml | 11 ++ kotlin/services/iam/build.gradle.kts | 2 + .../services/iam/src/test/kotlin/IAMTest.kt | 47 +++---- .../iam/src/test/resources/logback.xml | 11 ++ kotlin/services/iot/build.gradle.kts | 2 + .../services/iot/src/test/kotlin/IoTTest.kt | 9 +- .../iot/src/test/resources/logback.xml | 11 ++ kotlin/services/kendra/build.gradle.kts | 5 +- .../kendra/src/test/kotlin/KendraTest.kt | 42 ++---- .../kendra/src/test/resources/logback.xml | 11 ++ kotlin/services/keyspaces/build.gradle.kts | 2 + .../keyspaces/src/test/kotlin/KeyspaceTest.kt | 5 + .../keyspaces/src/test/resources/logback.xml | 11 ++ kotlin/services/kinesis/build.gradle.kts | 2 + .../kinesis/src/test/kotlin/KinesisTest.kt | 13 +- .../kinesis/src/test/resources/logback.xml | 11 ++ kotlin/services/kms/build.gradle.kts | 2 + .../kms/src/test/kotlin/KMSKotlinTest.kt | 61 ++++----- .../kms/src/test/resources/logback.xml | 11 ++ kotlin/services/lambda/build.gradle.kts | 2 + .../lambda/src/test/kotlin/LambdaTest.kt | 27 ++-- .../lambda/src/test/resources/logback.xml | 11 ++ kotlin/services/lex/build.gradle.kts | 2 + .../services/lex/src/test/kotlin/LexTest.kt | 24 ++-- .../lex/src/test/resources/logback.xml | 11 ++ kotlin/services/mediaconvert/build.gradle.kts | 2 + .../mediaconvert/src/test/kotlin/MCTest.kt | 25 ++-- .../src/test/resources/logback.xml | 11 ++ kotlin/services/mediastore/build.gradle.kts | 2 + .../src/test/kotlin/MediaStoreTest.kt | 15 ++- .../mediastore/src/test/resources/logback.xml | 11 ++ kotlin/services/opensearch/build.gradle.kts | 2 + .../src/test/kotlin/OpenSearchTest.kt | 9 +- .../opensearch/src/test/resources/logback.xml | 11 ++ kotlin/services/personalize/build.gradle.kts | 2 + .../src/test/kotlin/PersonalizeKotlinTest.kt | 39 ++---- .../src/test/resources/logback.xml | 11 ++ kotlin/services/pinpoint/build.gradle.kts | 2 + .../src/test/kotlin/PinpointKotlinTest.kt | 51 +++---- .../pinpoint/src/test/resources/logback.xml | 11 ++ kotlin/services/polly/build.gradle.kts | 2 + .../polly/src/test/kotlin/PollyKotlinTest.kt | 10 +- .../polly/src/test/resources/logback.xml | 11 ++ kotlin/services/rds/build.gradle.kts | 2 + .../services/rds/src/test/kotlin/RDSTest.kt | 86 +++--------- .../rds/src/test/resources/logback.xml | 11 ++ kotlin/services/redshift/build.gradle.kts | 2 + .../src/test/kotlin/RedshiftKotlinTest.kt | 19 +-- .../redshift/src/test/resources/logback.xml | 11 ++ kotlin/services/rekognition/build.gradle.kts | 2 + .../rekognition/VideoDetectInappropriate.kt | 2 +- .../src/test/kotlin/RekognitionTest.kt | 127 +++--------------- .../src/test/resources/logback.xml | 11 ++ kotlin/services/route53/build.gradle.kts | 2 + .../route53/src/test/kotlin/Route53Test.kt | 71 ++-------- .../route53/src/test/resources/logback.xml | 11 ++ .../s3/src/test/resources/logback.xml | 11 ++ kotlin/services/sagemaker/build.gradle.kts | 2 + .../src/test/kotlin/SageMakerTest.kt | 43 ++---- .../sagemaker/src/test/resources/logback.xml | 11 ++ .../services/secrets-manager/build.gradle.kts | 2 + .../test/kotlin/SecretsManagerKotlinTest.kt | 5 +- .../src/test/resources/logback.xml | 11 ++ kotlin/services/ses/build.gradle.kts | 2 + .../services/ses/src/test/kotlin/SESTest.kt | 27 +--- .../ses/src/test/resources/logback.xml | 11 ++ kotlin/services/sns/build.gradle.kts | 2 + .../services/sns/src/test/kotlin/SNSTest.kt | 47 +++---- .../sns/src/test/resources/logback.xml | 11 ++ kotlin/services/sqs/build.gradle.kts | 2 + .../services/sqs/src/test/kotlin/SQSTest.kt | 33 ++--- .../sqs/src/test/resources/logback.xml | 11 ++ .../services/stepfunctions/build.gradle.kts | 2 + .../test/kotlin/StepFunctionsKotlinTest.kt | 19 +-- .../src/test/resources/logback.xml | 11 ++ kotlin/services/sts/build.gradle.kts | 2 + .../src/test/kotlin/{Test2.kt => STSTest.kt} | 34 ++--- .../sts/src/test/resources/logback.xml | 11 ++ kotlin/services/support/build.gradle.kts | 2 + .../support/src/test/kotlin/SupportTest.kt | 6 +- .../support/src/test/resources/logback.xml | 11 ++ kotlin/services/textract/build.gradle.kts | 2 + .../textract/src/test/kotlin/TextractTest.kt | 26 +--- .../textract/src/test/resources/logback.xml | 11 ++ kotlin/services/translate/build.gradle.kts | 2 + .../src/test/kotlin/TranslateKotlinTest.kt | 25 +--- .../translate/src/test/resources/logback.xml | 11 ++ kotlin/services/xray/build.gradle.kts | 2 + .../xray/src/test/kotlin/XrayKotlinTest.kt | 28 ++-- .../xray/src/test/resources/logback.xml | 11 ++ 141 files changed, 1071 insertions(+), 1091 deletions(-) create mode 100644 kotlin/services/athena/src/test/resources/logback.xml create mode 100644 kotlin/services/cloudformation/src/test/resources/logback.xml create mode 100644 kotlin/services/cloudtrail/src/test/resources/logback.xml create mode 100644 kotlin/services/cloudwatch/src/test/resources/logback.xml delete mode 100644 kotlin/services/codepipeline/src/test/resources/config.properties create mode 100644 kotlin/services/codepipeline/src/test/resources/logback.xml create mode 100644 kotlin/services/cognito/src/test/resources/logback.xml create mode 100644 kotlin/services/comprehend/src/test/resources/logback.xml create mode 100644 kotlin/services/dynamodb/src/test/resources/logback.xml create mode 100644 kotlin/services/ec2/src/test/resources/logback.xml create mode 100644 kotlin/services/ecr/src/test/resources/logback.xml create mode 100644 kotlin/services/ecs/src/test/resources/logback.xml create mode 100644 kotlin/services/elasticbeanstalk/src/test/resources/logback.xml create mode 100644 kotlin/services/emr/src/test/resources/logback.xml create mode 100644 kotlin/services/eventbridge/src/test/resources/logback.xml create mode 100644 kotlin/services/firehose/src/test/resources/logback.xml create mode 100644 kotlin/services/forecast/src/test/resources/logback.xml create mode 100644 kotlin/services/glue/src/test/resources/logback.xml create mode 100644 kotlin/services/iam/src/test/resources/logback.xml create mode 100644 kotlin/services/iot/src/test/resources/logback.xml create mode 100644 kotlin/services/kendra/src/test/resources/logback.xml create mode 100644 kotlin/services/keyspaces/src/test/resources/logback.xml create mode 100644 kotlin/services/kinesis/src/test/resources/logback.xml create mode 100644 kotlin/services/kms/src/test/resources/logback.xml create mode 100644 kotlin/services/lambda/src/test/resources/logback.xml create mode 100644 kotlin/services/lex/src/test/resources/logback.xml create mode 100644 kotlin/services/mediaconvert/src/test/resources/logback.xml create mode 100644 kotlin/services/mediastore/src/test/resources/logback.xml create mode 100644 kotlin/services/opensearch/src/test/resources/logback.xml create mode 100644 kotlin/services/personalize/src/test/resources/logback.xml create mode 100644 kotlin/services/pinpoint/src/test/resources/logback.xml create mode 100644 kotlin/services/polly/src/test/resources/logback.xml create mode 100644 kotlin/services/rds/src/test/resources/logback.xml create mode 100644 kotlin/services/redshift/src/test/resources/logback.xml create mode 100644 kotlin/services/rekognition/src/test/resources/logback.xml create mode 100644 kotlin/services/route53/src/test/resources/logback.xml create mode 100644 kotlin/services/s3/src/test/resources/logback.xml create mode 100644 kotlin/services/sagemaker/src/test/resources/logback.xml create mode 100644 kotlin/services/secrets-manager/src/test/resources/logback.xml create mode 100644 kotlin/services/ses/src/test/resources/logback.xml create mode 100644 kotlin/services/sns/src/test/resources/logback.xml create mode 100644 kotlin/services/sqs/src/test/resources/logback.xml create mode 100644 kotlin/services/stepfunctions/src/test/resources/logback.xml rename kotlin/services/sts/src/test/kotlin/{Test2.kt => STSTest.kt} (78%) create mode 100644 kotlin/services/sts/src/test/resources/logback.xml create mode 100644 kotlin/services/support/src/test/resources/logback.xml create mode 100644 kotlin/services/textract/src/test/resources/logback.xml create mode 100644 kotlin/services/translate/src/test/resources/logback.xml create mode 100644 kotlin/services/xray/src/test/resources/logback.xml diff --git a/kotlin/services/athena/build.gradle.kts b/kotlin/services/athena/build.gradle.kts index 1e517fd31ea..8508a196f57 100644 --- a/kotlin/services/athena/build.gradle.kts +++ b/kotlin/services/athena/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/athena/src/test/kotlin/AthenaTest.kt b/kotlin/services/athena/src/test/kotlin/AthenaTest.kt index 3ed67ee37e7..3775eea6000 100644 --- a/kotlin/services/athena/src/test/kotlin/AthenaTest.kt +++ b/kotlin/services/athena/src/test/kotlin/AthenaTest.kt @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -23,10 +22,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class AthenaTest { + private val logger: Logger = LoggerFactory.getLogger(AthenaTest::class.java) private var nameQuery: String? = null private var queryString: String? = null private var database: String? = null @@ -43,17 +45,6 @@ class AthenaTest { queryString = values.queryString.toString() database = values.database.toString() outputLocation = values.outputLocation.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - nameQuery = prop.getProperty("nameQuery") - queryString = prop.getProperty("queryString") - database = prop.getProperty("database") - outputLocation = prop.getProperty("outputLocation") - */ } @Test @@ -61,21 +52,21 @@ class AthenaTest { fun createNamedQueryTest() = runBlocking { queryId = createNamedQuery(queryString.toString(), nameQuery.toString(), database.toString()) queryId?.let { assertTrue(it.isNotEmpty()) } - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @Order(2) fun listNamedQueryTest() = runBlocking { listNamedQueries() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @Order(3) fun listQueryExecutionsTest() = runBlocking { listQueryIds() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -84,14 +75,14 @@ class AthenaTest { val queryExecutionId = submitAthenaQuery(queryString.toString(), database.toString(), outputLocation.toString()) waitForQueryToComplete(queryExecutionId) processResultRows(queryExecutionId) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @Order(5) fun deleteNamedQueryTest() = runBlocking { deleteQueryName(queryId) - println("Test 5 passed") + logger.info("Test 5 passed") } private suspend fun getSecretValues(): String { @@ -101,7 +92,6 @@ class AthenaTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/athena/src/test/resources/logback.xml b/kotlin/services/athena/src/test/resources/logback.xml new file mode 100644 index 00000000000..3b326892915 --- /dev/null +++ b/kotlin/services/athena/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %level %logger{36} - %msg%n + + + + + + + diff --git a/kotlin/services/cloudformation/build.gradle.kts b/kotlin/services/cloudformation/build.gradle.kts index 70a4f330a59..762c8c88e0f 100644 --- a/kotlin/services/cloudformation/build.gradle.kts +++ b/kotlin/services/cloudformation/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt b/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt index ae572c94973..27547a7fc60 100644 --- a/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt +++ b/kotlin/services/cloudformation/src/test/kotlin/CloudFormationTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -18,15 +17,16 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class CloudFormationTest { + private val logger: Logger = LoggerFactory.getLogger(CloudFormationTest::class.java) private var stackName = "" private var roleARN = "" private var location = "" - private var key = "" - private var value = "" @BeforeAll fun setup() = @@ -38,17 +38,6 @@ class CloudFormationTest { stackName = values.stackName.toString() roleARN = values.roleARN.toString() location = values.location.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - stackName = prop.getProperty("stackName") - roleARN = prop.getProperty("roleARN") - location = prop.getProperty("location") - key = prop.getProperty("key") - value = prop.getProperty("value") - */ } @Test @@ -56,7 +45,7 @@ class CloudFormationTest { fun createStackTest() = runBlocking { createCFStack(stackName, roleARN, location) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -64,7 +53,7 @@ class CloudFormationTest { fun describeStacksTest() = runBlocking { describeAllStacks() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -72,7 +61,7 @@ class CloudFormationTest { fun getTemplateTest() = runBlocking { getSpecificTemplate(stackName) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -80,7 +69,7 @@ class CloudFormationTest { fun deleteStackTest() = runBlocking { deleteSpecificTemplate(stackName) - println("Test 4 passed") + logger.info("Test 4 passed") } private suspend fun getSecretValues(): String { @@ -91,7 +80,6 @@ class CloudFormationTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/cloudformation/src/test/resources/logback.xml b/kotlin/services/cloudformation/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/cloudformation/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/cloudtrail/build.gradle.kts b/kotlin/services/cloudtrail/build.gradle.kts index 1ea9ca8fe62..c2358d65c3f 100644 --- a/kotlin/services/cloudtrail/build.gradle.kts +++ b/kotlin/services/cloudtrail/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/cloudtrail/src/test/kotlin/CloudtrailKotlinTest.kt b/kotlin/services/cloudtrail/src/test/kotlin/CloudtrailKotlinTest.kt index aa75acfc5b1..5847d02acf9 100644 --- a/kotlin/services/cloudtrail/src/test/kotlin/CloudtrailKotlinTest.kt +++ b/kotlin/services/cloudtrail/src/test/kotlin/CloudtrailKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -22,10 +21,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class CloudtrailKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(CloudtrailKotlinTest::class.java) private var trailName = "" private var s3BucketName = "" @@ -37,17 +39,6 @@ class CloudtrailKotlinTest { val values: SecretValues = gson.fromJson(json, SecretValues::class.java) trailName = values.trailName.toString() s3BucketName = values.s3BucketName.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // load the properties file. - prop.load(input) - trailName = prop.getProperty("trailName") - s3BucketName = prop.getProperty("s3BucketName") - */ } @Test @@ -55,7 +46,7 @@ class CloudtrailKotlinTest { fun createTrail() = runBlocking { createNewTrail(trailName, s3BucketName) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -63,7 +54,7 @@ class CloudtrailKotlinTest { fun putEventSelectors() = runBlocking { setSelector(trailName) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -71,7 +62,7 @@ class CloudtrailKotlinTest { fun getEventSelectors() = runBlocking { getSelectors(trailName) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -79,7 +70,7 @@ class CloudtrailKotlinTest { fun lookupEvents() = runBlocking { lookupAllEvents() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -87,7 +78,7 @@ class CloudtrailKotlinTest { fun describeTrails() = runBlocking { describeSpecificTrails(trailName) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -96,7 +87,7 @@ class CloudtrailKotlinTest { runBlocking { startLog(trailName) stopLog(trailName) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -104,7 +95,7 @@ class CloudtrailKotlinTest { fun deleteTrail() = runBlocking { deleteSpecificTrail(trailName) - println("Test 7 passed") + logger.info("Test 7 passed") } private suspend fun getSecretValues(): String { @@ -115,7 +106,6 @@ class CloudtrailKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/cloudtrail/src/test/resources/logback.xml b/kotlin/services/cloudtrail/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/cloudtrail/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/cloudwatch/build.gradle.kts b/kotlin/services/cloudwatch/build.gradle.kts index 5715a0a9f02..e175829145e 100644 --- a/kotlin/services/cloudwatch/build.gradle.kts +++ b/kotlin/services/cloudwatch/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/cloudwatch/src/test/kotlin/CloudWatchTest.kt b/kotlin/services/cloudwatch/src/test/kotlin/CloudWatchTest.kt index ef88efa4a46..08ea9a65c84 100644 --- a/kotlin/services/cloudwatch/src/test/kotlin/CloudWatchTest.kt +++ b/kotlin/services/cloudwatch/src/test/kotlin/CloudWatchTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -27,10 +26,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class CloudWatchTest { + private val logger: Logger = LoggerFactory.getLogger(CloudWatchTest::class.java) private var logGroup = "" private var alarmName = "" private var streamName = "" @@ -79,33 +81,6 @@ class CloudWatchTest { dashboardAddSc = values.dashboardAddSc.toString() settingsSc = values.settingsSc.toString() metricImageSc = values.metricImageSc.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - logGroup = prop.getProperty("logGroup") - alarmName = prop.getProperty("alarmName") - streamName = prop.getProperty("streamName") - ruleResource = prop.getProperty("ruleResource") - metricId = prop.getProperty("metricId") - filterName = prop.getProperty("filterName") - destinationArn = prop.getProperty("destinationArn") - roleArn = prop.getProperty("roleArn") - filterPattern = prop.getProperty("filterPattern") - instanceId = prop.getProperty("instanceId") - ruleName = prop.getProperty("ruleName") - ruleArn = prop.getProperty("ruleArn") - namespace = prop.getProperty("namespace") - myDateSc = prop.getProperty("myDateSc") - costDateWeekSc = prop.getProperty("costDateWeekSc") - dashboardNameSc = prop.getProperty("dashboardNameSc") - dashboardJsonSc = prop.getProperty("dashboardJsonSc") - dashboardAddSc = prop.getProperty("dashboardAddSc") - settingsSc = prop.getProperty("settingsSc") - metricImageSc = prop.getProperty("metricImageSc") - */ } @Test @@ -113,7 +88,7 @@ class CloudWatchTest { fun createAlarmTest() = runBlocking { putAlarm(alarmName, instanceId) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -121,7 +96,7 @@ class CloudWatchTest { fun describeAlarmsTest() = runBlocking { desCWAlarms() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -129,7 +104,7 @@ class CloudWatchTest { fun createSubscriptionFiltersTest() = runBlocking { putSubFilters(filterName, filterPattern, logGroup, destinationArn) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -137,7 +112,7 @@ class CloudWatchTest { fun describeSubscriptionFiltersTest() = runBlocking { describeFilters(logGroup) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -145,7 +120,7 @@ class CloudWatchTest { fun disableAlarmActionsTest() = runBlocking { disableActions(alarmName) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -153,7 +128,7 @@ class CloudWatchTest { fun enableAlarmActionsTest() = runBlocking { enableActions(alarmName) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -161,7 +136,7 @@ class CloudWatchTest { fun getLogEventsTest() = runBlocking { getCWLogEvents(logGroup, streamName) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -169,7 +144,7 @@ class CloudWatchTest { fun putCloudWatchEventTest() = runBlocking { putCWEvents(ruleResource) - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -177,7 +152,7 @@ class CloudWatchTest { fun getMetricDataTest() = runBlocking { getMetData() - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -185,7 +160,7 @@ class CloudWatchTest { fun deleteSubscriptionFilterTest() = runBlocking { deleteSubFilter(filterName, logGroup) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -193,7 +168,7 @@ class CloudWatchTest { fun putRuleTest() = runBlocking { putCWRule(ruleName, ruleArn) - println("Test 11 passed") + logger.info("Test 11 passed") } @Test @@ -201,7 +176,7 @@ class CloudWatchTest { fun putLogEvents() = runBlocking { putCWLogEvents(logGroup, streamName) - println("Test 12 passed") + logger.info("Test 12 passed") } @Test @@ -209,7 +184,7 @@ class CloudWatchTest { fun deleteCWAlarmTest() = runBlocking { deleteCWAlarm(alarmName) - println("Test 13 passed") + logger.info("Test 13 passed") } private suspend fun getSecretValues(): String { @@ -220,7 +195,6 @@ class CloudWatchTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/cloudwatch/src/test/resources/logback.xml b/kotlin/services/cloudwatch/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/cloudwatch/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/codepipeline/build.gradle.kts b/kotlin/services/codepipeline/build.gradle.kts index 367b95dc0a4..f21e509d4ee 100644 --- a/kotlin/services/codepipeline/build.gradle.kts +++ b/kotlin/services/codepipeline/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/codepipeline/src/test/kotlin/PipelineServiceTest.kt b/kotlin/services/codepipeline/src/test/kotlin/PipelineServiceTest.kt index 98b92df33ff..b62297ac2e2 100644 --- a/kotlin/services/codepipeline/src/test/kotlin/PipelineServiceTest.kt +++ b/kotlin/services/codepipeline/src/test/kotlin/PipelineServiceTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -20,10 +19,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class PipelineServiceTest { + private val logger: Logger = LoggerFactory.getLogger(PipelineServiceTest::class.java) private var name: String = "" private var roleArn: String = "" private var s3Bucket: String = "" @@ -47,7 +49,7 @@ class PipelineServiceTest { fun createPipelineTest() = runBlocking { createNewPipeline(name, roleArn, s3Bucket, s3OutputBucket) - println("\n Test 1 passed") + logger.info("\n Test 1 passed") } @Test @@ -55,7 +57,7 @@ class PipelineServiceTest { fun startPipelineExecutionTest() = runBlocking { executePipeline(name) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -63,7 +65,7 @@ class PipelineServiceTest { fun listPipelinesTest() = runBlocking { getAllPipelines() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -71,7 +73,7 @@ class PipelineServiceTest { fun getPipelineTest() = runBlocking { getSpecificPipeline(name) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -79,7 +81,7 @@ class PipelineServiceTest { fun listPipelineExecutionsTest() = runBlocking { listExecutions(name) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -87,7 +89,7 @@ class PipelineServiceTest { fun deletePipelineTest() = runBlocking { deleteSpecificPipeline(name) - println("Test 6 passed") + logger.info("Test 6 passed") } private suspend fun getSecretValues(): String { @@ -98,7 +100,6 @@ class PipelineServiceTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/codepipeline/src/test/resources/config.properties b/kotlin/services/codepipeline/src/test/resources/config.properties deleted file mode 100644 index 090967de2b0..00000000000 --- a/kotlin/services/codepipeline/src/test/resources/config.properties +++ /dev/null @@ -1,4 +0,0 @@ -roleArn = -name = -s3Bucket = -s3OuputBucket = \ No newline at end of file diff --git a/kotlin/services/codepipeline/src/test/resources/logback.xml b/kotlin/services/codepipeline/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/codepipeline/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/cognito/build.gradle.kts b/kotlin/services/cognito/build.gradle.kts index 57f51b4bef7..61d0a1c133e 100644 --- a/kotlin/services/cognito/build.gradle.kts +++ b/kotlin/services/cognito/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("com.google.code.gson:gson:2.10") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt b/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt index 2d5b06ca5d6..9c780c5cc13 100644 --- a/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt +++ b/kotlin/services/cognito/src/test/kotlin/CognitoKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -25,11 +24,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class CognitoKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(CognitoKotlinTest::class.java) private var userPoolName = "" private var identityId = "" private var userPoolId = "" // set in test 2 @@ -77,34 +79,6 @@ class CognitoKotlinTest { userNameMVP = values.userNameMVP.toString() passwordMVP = values.passwordMVP.toString() emailMVP = values.emailMVP.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - /* - // load the properties file. - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - userPoolName = prop.getProperty("userPoolName") - identityId = prop.getProperty("identityId") - username = prop.getProperty("username") - email = prop.getProperty("email") - clientName = prop.getProperty("clientName") - identityPoolName = prop.getProperty("identityPoolName") - appId = prop.getProperty("appId") - existingUserPoolId = prop.getProperty("existingUserPoolId") - existingIdentityPoolId = prop.getProperty("existingIdentityPoolId") - providerName = prop.getProperty("providerName") - existingPoolName = prop.getProperty("existingPoolName") - clientId = prop.getProperty("clientId") - secretkey = prop.getProperty("secretkey") - password = prop.getProperty("password") - poolIdMVP = prop.getProperty("poolIdMVP") - clientIdMVP = prop.getProperty("clientIdMVP") - userNameMVP = prop.getProperty("userNameMVP") - passwordMVP = prop.getProperty("passwordMVP") - emailMVP = prop.getProperty("emailMVP") - - */ } @Test @@ -113,7 +87,7 @@ class CognitoKotlinTest { runBlocking { userPoolId = createPool(userPoolName).toString() Assertions.assertTrue(!userPoolId.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -121,7 +95,7 @@ class CognitoKotlinTest { fun createAdminUserTest() = runBlocking { createNewUser(userPoolId, username, email, password) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -129,7 +103,7 @@ class CognitoKotlinTest { fun listUserPoolsTest() = runBlocking { getAllPools() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -137,7 +111,7 @@ class CognitoKotlinTest { fun listUserPoolClientsTest() = runBlocking { listAllUserPoolClients(existingUserPoolId) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -145,7 +119,7 @@ class CognitoKotlinTest { fun listUsersTest() = runBlocking { listAllUserPoolClients(existingUserPoolId) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -153,7 +127,7 @@ class CognitoKotlinTest { fun describeUserPoolTest() = runBlocking { describePool(existingUserPoolId) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -161,7 +135,7 @@ class CognitoKotlinTest { fun deleteUserPool() = runBlocking { delPool(userPoolId) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -170,7 +144,7 @@ class CognitoKotlinTest { runBlocking { identityPoolId = createIdPool(identityPoolName).toString() Assertions.assertTrue(!identityPoolId.isEmpty()) - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -178,7 +152,7 @@ class CognitoKotlinTest { fun listIdentityProvidersTest() = runBlocking { getPools() - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -186,7 +160,7 @@ class CognitoKotlinTest { fun listIdentitiesTest() = runBlocking { listPoolIdentities(identityPoolId) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -194,14 +168,13 @@ class CognitoKotlinTest { fun deleteIdentityPool() = runBlocking { deleteIdPool(identityPoolId) - println("Test 11 passed") + logger.info("Test 11 passed") } private suspend fun getSecretValues(): String { val secretClient = SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() } val secretName = "test/cognito" val valueRequest = diff --git a/kotlin/services/cognito/src/test/resources/logback.xml b/kotlin/services/cognito/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/cognito/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/comprehend/build.gradle.kts b/kotlin/services/comprehend/build.gradle.kts index 28ca4042626..d625af30796 100644 --- a/kotlin/services/comprehend/build.gradle.kts +++ b/kotlin/services/comprehend/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/comprehend/src/test/java/ComprehendKotlinTest.kt b/kotlin/services/comprehend/src/test/java/ComprehendKotlinTest.kt index 59722496f81..331996bfe43 100644 --- a/kotlin/services/comprehend/src/test/java/ComprehendKotlinTest.kt +++ b/kotlin/services/comprehend/src/test/java/ComprehendKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -19,10 +18,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class ComprehendKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(ComprehendKotlinTest::class.java) private val text = """ Amazon.com, Inc. is located in Seattle, WA and was founded July 5th, 1994 by Jeff Bezos, allowing customers to buy everything from books to blenders. @@ -43,21 +45,6 @@ Seattle is north of Portland and south of Vancouver, BC. Other notable Seattle - dataAccessRoleArn = values.dataAccessRoleArn.toString() s3Uri = values.s3Uri.toString() documentClassifierName = values.documentClassifier.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // load the properties file. - prop.load(input) - - // Populate the data members required for all tests. - dataAccessRoleArn = prop.getProperty("dataAccessRoleArn") - s3Uri = prop.getProperty("s3Uri") - documentClassifierName = prop.getProperty("documentClassifier") - */ } @Test @@ -65,7 +52,7 @@ Seattle is north of Portland and south of Vancouver, BC. Other notable Seattle - fun detectEntitiesTest() = runBlocking { detectAllEntities(text) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -73,7 +60,7 @@ Seattle is north of Portland and south of Vancouver, BC. Other notable Seattle - fun detectKeyPhrasesTest() = runBlocking { detectAllKeyPhrases(text) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -81,7 +68,7 @@ Seattle is north of Portland and south of Vancouver, BC. Other notable Seattle - fun detectLanguageTest() = runBlocking { detectTheDominantLanguage(frText) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -89,7 +76,7 @@ Seattle is north of Portland and south of Vancouver, BC. Other notable Seattle - fun detectSentimentTest() = runBlocking { detectSentiments(text) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -97,14 +84,13 @@ Seattle is north of Portland and south of Vancouver, BC. Other notable Seattle - fun detectSyntaxTest() = runBlocking { detectAllSyntax(text) - println("Test 5 passed") + logger.info("Test 5 passed") } private suspend fun getSecretValues(): String { val secretClient = SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() } val secretName = "test/comprehend" val valueRequest = diff --git a/kotlin/services/comprehend/src/test/resources/logback.xml b/kotlin/services/comprehend/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/comprehend/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/dynamodb/build.gradle.kts b/kotlin/services/dynamodb/build.gradle.kts index 1d6bfc1f817..e96691e6290 100644 --- a/kotlin/services/dynamodb/build.gradle.kts +++ b/kotlin/services/dynamodb/build.gradle.kts @@ -39,6 +39,8 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") implementation("com.google.code.gson:gson:2.10.1") implementation("com.googlecode.json-simple:json-simple:1.1.1") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/dynamodb/src/test/kotlin/DynamoDB.kt b/kotlin/services/dynamodb/src/test/kotlin/DynamoDB.kt index 28ccbb33f2b..edd7794c709 100644 --- a/kotlin/services/dynamodb/src/test/kotlin/DynamoDB.kt +++ b/kotlin/services/dynamodb/src/test/kotlin/DynamoDB.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.dynamodb.DynamoDbClient import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest @@ -41,10 +40,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class DynamoDB { + private val logger: Logger = LoggerFactory.getLogger(DynamoDB::class.java) var tableName: String = "" var fileName: String = "" var tableName2: String = "" @@ -76,27 +78,6 @@ class DynamoDB { songTitle = values.songTitleVal.toString() songTitleVal = values.songTitleVal.toString() tableName2 = "Movies" - - /* - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = java.util.Properties() - - // load the properties file. - prop.load(input) - tableName = prop.getProperty("tableName") - tableName2 = prop.getProperty("tableName2") - fileName = prop.getProperty("fileName") - key = prop.getProperty("key") - keyValue = prop.getProperty("keyValue") - albumTitle = prop.getProperty("albumTitle") - albumTitleValue = prop.getProperty("albumTitleValue") - awards = prop.getProperty("awards") - awardVal = prop.getProperty("awardVal") - songTitle = prop.getProperty("songTitle") - songTitleVal = prop.getProperty("songTitleVal") - modAwardVal = prop.getProperty("modAwardVal") - */ } @Test @@ -104,7 +85,7 @@ class DynamoDB { fun createTableTest() = runBlocking { createNewTable(tableName, key) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -112,7 +93,7 @@ class DynamoDB { fun describeTableTest() = runBlocking { describeDymamoDBTable(tableName) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -130,7 +111,7 @@ class DynamoDB { songTitle, songTitleVal, ) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -138,7 +119,7 @@ class DynamoDB { fun listTablesTest() = runBlocking { listAllTables() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -146,7 +127,7 @@ class DynamoDB { fun updateItemTest() = runBlocking { updateTableItem(tableName, key, keyValue, awards, modAwardVal) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -154,7 +135,7 @@ class DynamoDB { fun getItemTest() = runBlocking { getSpecificItem(tableName, key, keyValue) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -162,7 +143,7 @@ class DynamoDB { fun queryTableTest() = runBlocking { queryDynTable(tableName, key, keyValue, "#a") - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -170,7 +151,7 @@ class DynamoDB { fun dynamoDBScanTest() = runBlocking { scanItems(tableName) - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -178,7 +159,7 @@ class DynamoDB { fun deleteItemTest() = runBlocking { com.kotlin.dynamodb.deleteDynamoDBItem(tableName, key, keyValue) - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -186,7 +167,7 @@ class DynamoDB { fun deleteTableTest() = runBlocking { deleteDynamoDBTable(tableName) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -198,6 +179,7 @@ class DynamoDB { getMovie(tableName2, "year", "1933") scanMovies(tableName2) deletIssuesTable(tableName2) + logger.info("Test 11 passed") } @Test @@ -213,6 +195,7 @@ class DynamoDB { updateTableItemPartiQL(ddb) queryTablePartiQL(ddb) deleteTablePartiQL(tableNamePartiQ) + logger.info("Test 12 passed") } @Test @@ -227,13 +210,13 @@ class DynamoDB { updateTableItemBatchBatch(ddb) deleteItemsBatch(ddb) deleteTablePartiQLBatch(tableNamePartiQBatch) + logger.info("Test 13 passed") } private suspend fun getSecretValues(): String { val secretClient = SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() } val secretName = "test/dynamodb" val valueRequest = diff --git a/kotlin/services/dynamodb/src/test/resources/logback.xml b/kotlin/services/dynamodb/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/dynamodb/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/ec2/build.gradle.kts b/kotlin/services/ec2/build.gradle.kts index 9643febab98..9b7a34b844c 100644 --- a/kotlin/services/ec2/build.gradle.kts +++ b/kotlin/services/ec2/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/ec2/src/test/kotlin/EC2Test.kt b/kotlin/services/ec2/src/test/kotlin/EC2Test.kt index aed25c8bd84..41ed9e91e22 100644 --- a/kotlin/services/ec2/src/test/kotlin/EC2Test.kt +++ b/kotlin/services/ec2/src/test/kotlin/EC2Test.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -29,12 +28,15 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.io.IOException import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class EC2Test { + private val logger: Logger = LoggerFactory.getLogger(EC2Test::class.java) private var instanceId = "" // Gets set in test 2. private var ami = "" private var instanceName = "" @@ -73,38 +75,6 @@ class EC2Test { groupNameSc = values.groupNameSc.toString() + randomNum vpcIdSc = values.vpcIdSc.toString() myIpAddressSc = values.myIpAddressSc.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - try { - EC2Test::class.java.classLoader.getResourceAsStream("config.properties").use { input -> - val prop = Properties() - if (input == null) { - println("Sorry, unable to find config.properties") - return - } - prop.load(input) - - // Populate the data members required for all tests. - ami = prop.getProperty("ami") - instanceName = prop.getProperty("instanceName") - keyName = prop.getProperty("keyName") - groupName = prop.getProperty("groupName") - groupDesc = prop.getProperty("groupDesc") - vpcId = prop.getProperty("vpcId") - - keyNameSc = prop.getProperty("keyNameSc") - fileNameSc = prop.getProperty("fileNameSc") - groupDescSc = prop.getProperty("groupDescSc") - groupNameSc = prop.getProperty("groupNameSc") - vpcIdSc = prop.getProperty("vpcIdSc") - myIpAddressSc = prop.getProperty("myIpAddressSc") - } - } catch (ex: IOException) { - ex.printStackTrace() - } - */ } @Test @@ -113,7 +83,7 @@ class EC2Test { runBlocking { instanceId = createEC2Instance(instanceName, ami).toString() assertTrue(instanceId.isNotEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -121,7 +91,7 @@ class EC2Test { fun createKeyPairTest() = runBlocking { createEC2KeyPair(keyName) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -129,7 +99,7 @@ class EC2Test { fun describeKeyPairTest() = runBlocking { describeEC2Keys() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -137,7 +107,7 @@ class EC2Test { fun deleteKeyPairTest() = runBlocking { deleteKeys(keyName) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -146,7 +116,7 @@ class EC2Test { runBlocking { groupId = createEC2SecurityGroup(groupName, groupDesc, vpcId).toString() assertTrue(groupId.isNotEmpty()) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -154,7 +124,7 @@ class EC2Test { fun describeSecurityGroupTest() = runBlocking { describeEC2SecurityGroups(groupId) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -162,7 +132,7 @@ class EC2Test { fun deleteSecurityGroupTest() = runBlocking { deleteEC2SecGroup(groupId) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -170,7 +140,7 @@ class EC2Test { fun describeAccountTest() = runBlocking { describeEC2Account() - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -178,7 +148,7 @@ class EC2Test { fun describeInstancesTest() = runBlocking { describeEC2Instances() - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -186,7 +156,7 @@ class EC2Test { fun describeRegionsAndZonesTest() = runBlocking { describeEC2RegionsAndZones() - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -194,7 +164,7 @@ class EC2Test { fun describeVPCsTest() = runBlocking { describeEC2Vpcs(vpcId) - println("Test 11 passed") + logger.info("Test 11 passed") } @Test @@ -202,7 +172,7 @@ class EC2Test { fun findRunningInstancesTest() = runBlocking { findRunningEC2Instances() - println("Test 12 passed") + logger.info("Test 12 passed") } @Test @@ -210,7 +180,7 @@ class EC2Test { fun describeAddressesTest() = runBlocking { describeEC2Address() - println("Test 13 passed") + logger.info("Test 13 passed") } @Test @@ -218,7 +188,7 @@ class EC2Test { fun terminateInstanceTest() = runBlocking { terminateEC2(instanceId) - println("Test 14 passed") + logger.info("Test 14 passed") } private suspend fun getSecretValues(): String { @@ -229,7 +199,6 @@ class EC2Test { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/ec2/src/test/resources/logback.xml b/kotlin/services/ec2/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/ec2/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/ecr/build.gradle.kts b/kotlin/services/ecr/build.gradle.kts index 95172f834b0..8f953929622 100644 --- a/kotlin/services/ecr/build.gradle.kts +++ b/kotlin/services/ecr/build.gradle.kts @@ -39,7 +39,8 @@ dependencies { implementation("com.github.docker-java:docker-java-core:3.3.6") implementation("com.github.docker-java:docker-java-transport-httpclient5:3.3.6") implementation("com.github.docker-java:docker-java:3.3.6") - // implementation("ch.qos.logback:logback-classic:1.2.11") // Updated Logback version + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/ecr/src/test/kotlin/ECRTest.kt b/kotlin/services/ecr/src/test/kotlin/ECRTest.kt index 7660f08b155..043b12928af 100644 --- a/kotlin/services/ecr/src/test/kotlin/ECRTest.kt +++ b/kotlin/services/ecr/src/test/kotlin/ECRTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.example.ecr.listImageTags @@ -17,10 +16,13 @@ import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class ECRTest { + private val logger: Logger = LoggerFactory.getLogger(ECRTest::class.java) private var repoName = "" private var newRepoName = "" private var iamRole = "" @@ -39,35 +41,19 @@ class ECRTest { repoName = values.existingRepo.toString() } - @Test - @Order(1) - fun testScenario() = - runBlocking { - ecrActions?.createECRRepository(newRepoName) - ecrActions?.setRepoPolicy(newRepoName, iamRole) - ecrActions?.getRepoPolicy(newRepoName) - ecrActions?.getRepositoryURI(newRepoName) - ecrActions?.setLifeCyclePolicy(newRepoName) - ecrActions?.pushDockerImage(newRepoName, newRepoName) - ecrActions?.verifyImage(newRepoName, newRepoName) - ecrActions?.deleteECRRepository(newRepoName) - println("Test 1 passed") - } - @Test @Tag("IntegrationTest") - @Order(2) + @Order(1) fun testHello() = runBlocking { listImageTags(repoName) - println("Test 2 passed") + logger.info("Test 1 passed") } private suspend fun getSecretValues(): String { val secretClient = SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() } val secretName = "test/ecr" val valueRequest = diff --git a/kotlin/services/ecr/src/test/resources/logback.xml b/kotlin/services/ecr/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/ecr/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/ecs/build.gradle.kts b/kotlin/services/ecs/build.gradle.kts index 87e01bffab3..883da7c1ac5 100644 --- a/kotlin/services/ecs/build.gradle.kts +++ b/kotlin/services/ecs/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.google.code.gson:gson:2.10") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/ecs/src/test/kotlin/ESCTest.kt b/kotlin/services/ecs/src/test/kotlin/ESCTest.kt index 2299866da3e..2e22d3a86f4 100644 --- a/kotlin/services/ecs/src/test/kotlin/ESCTest.kt +++ b/kotlin/services/ecs/src/test/kotlin/ESCTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -21,11 +20,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class ESCTest { + private val logger: Logger = LoggerFactory.getLogger(ESCTest::class.java) var clusterName = "" var clusterARN = "" var securityGroups: String = "" @@ -48,21 +50,6 @@ class ESCTest { securityGroups = values.securityGroups.toString() serviceName = values.serviceName.toString() + UUID.randomUUID() taskDefinition = values.taskDefinition.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - // val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - // val prop = Properties() - - // load the properties file. - // prop.load(input) - - // Populate the data members required for all tests - // clusterName = prop.getProperty("clusterName") - // taskId = prop.getProperty("taskId") - // subnet = prop.getProperty("subnet") - // securityGroups = prop.getProperty("securityGroups") - // serviceName = prop.getProperty("serviceName") - // taskDefinition = prop.getProperty("taskDefinition") } @Test @@ -70,7 +57,7 @@ class ESCTest { fun createClusterTest() = runBlocking { clusterARN = createGivenCluster(clusterName).toString() - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -78,7 +65,7 @@ class ESCTest { fun createServiceTest() = runBlocking { serviceArn = createNewService(clusterARN, serviceName, securityGroups, subnet, taskDefinition).toString() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -86,7 +73,7 @@ class ESCTest { fun listClustersTest() = runBlocking { listAllClusters() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -94,7 +81,7 @@ class ESCTest { fun describeClustersTest() = runBlocking { descCluster(clusterARN) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -102,7 +89,7 @@ class ESCTest { fun listTaskDefinitionsTest() = runBlocking { getAllTasks(clusterARN, taskId) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -110,7 +97,7 @@ class ESCTest { fun updateServiceTest() = runBlocking { updateSpecificService(clusterARN, serviceArn) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -118,14 +105,13 @@ class ESCTest { fun deleteServiceTest() = runBlocking { deleteSpecificService(clusterARN, serviceArn) - println("Test 7 passed") + logger.info("Test 7 passed") } private suspend fun getSecretValues(): String { val secretClient = SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() } val secretName = "test/ecs" val valueRequest = diff --git a/kotlin/services/ecs/src/test/resources/logback.xml b/kotlin/services/ecs/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/ecs/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/elasticbeanstalk/build.gradle.kts b/kotlin/services/elasticbeanstalk/build.gradle.kts index fa56641efc2..f9a4dfad8e6 100644 --- a/kotlin/services/elasticbeanstalk/build.gradle.kts +++ b/kotlin/services/elasticbeanstalk/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/elasticbeanstalk/src/test/kotlin/ElasticBeanstalkTest.kt b/kotlin/services/elasticbeanstalk/src/test/kotlin/ElasticBeanstalkTest.kt index 4d2628b90b5..2359788ffbe 100644 --- a/kotlin/services/elasticbeanstalk/src/test/kotlin/ElasticBeanstalkTest.kt +++ b/kotlin/services/elasticbeanstalk/src/test/kotlin/ElasticBeanstalkTest.kt @@ -16,6 +16,8 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.io.IOException import java.net.URISyntaxException import java.util.Random @@ -24,6 +26,7 @@ import java.util.concurrent.TimeUnit @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class ElasticBeanstalkTest { + private val logger: Logger = LoggerFactory.getLogger(ElasticBeanstalkTest::class.java) var appName: String = "TestApp" var envName: String = "TestEnv" var appArn: String = "" @@ -42,7 +45,7 @@ class ElasticBeanstalkTest { @Order(1) fun whenInitializingAWSService_thenNotNull() { Assertions.assertNotNull(appName) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -51,7 +54,7 @@ class ElasticBeanstalkTest { runBlocking { appArn = createApp(appName) assertTrue(!appArn.isEmpty()) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -60,7 +63,7 @@ class ElasticBeanstalkTest { runBlocking { envArn = createEBEnvironment(envName, appName) assertTrue(!envArn.isEmpty()) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -68,7 +71,7 @@ class ElasticBeanstalkTest { fun describeApplications() = runBlocking { describeApps() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -76,7 +79,7 @@ class ElasticBeanstalkTest { fun describeEnvironment() = runBlocking { describeEnv(appName) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -84,7 +87,7 @@ class ElasticBeanstalkTest { fun describeOptions() = runBlocking { getOptions(envName) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -94,6 +97,6 @@ class ElasticBeanstalkTest { println("*** Wait for 5 MIN so the app can be deleted") TimeUnit.MINUTES.sleep(5) deleteApp(appName) - println("Test 7 passed") + logger.info("Test 7 passed") } } diff --git a/kotlin/services/elasticbeanstalk/src/test/resources/logback.xml b/kotlin/services/elasticbeanstalk/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/elasticbeanstalk/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/emr/build.gradle.kts b/kotlin/services/emr/build.gradle.kts index 83e8ca4b359..a9e812ccb11 100644 --- a/kotlin/services/emr/build.gradle.kts +++ b/kotlin/services/emr/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/emr/src/test/kotlin/EMRTest.kt b/kotlin/services/emr/src/test/kotlin/EMRTest.kt index 8b08ec3c800..8de73b94eb3 100644 --- a/kotlin/services/emr/src/test/kotlin/EMRTest.kt +++ b/kotlin/services/emr/src/test/kotlin/EMRTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -15,17 +14,19 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.io.IOException @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class EMRTest { + private val logger: Logger = LoggerFactory.getLogger(EMRTest::class.java) private var jar = "" private var myClass = "" private var keys = "" private var logUri = "" private var name = "" - private var jobFlowId = "" private var existingClusterId = "" @BeforeAll @@ -42,31 +43,6 @@ class EMRTest { logUri = values.logUri.toString() name = values.name.toString() existingClusterId = values.existingClusterId.toString() - - /* - try { - EMRTest::class.java.classLoader.getResourceAsStream("config.properties").use { input -> - val prop = Properties() - if (input == null) { - println("Sorry, unable to find config.properties") - return - } - - // load a properties file from class path, inside static method - prop.load(input) - - // Populate the data members required for all tests - jar = prop.getProperty("jar") - myClass = prop.getProperty("myClass") - keys = prop.getProperty("keys") - logUri = prop.getProperty("logUri") - name = prop.getProperty("name") - existingClusterId = prop.getProperty("existingClusterId") - } - } catch (ex: IOException) { - ex.printStackTrace() - } - */ } @Test @@ -74,7 +50,7 @@ class EMRTest { fun listClustersTest() = runBlocking { listAllClusters() - println("Test 3 passed") + logger.info("Test 1 passed") } private suspend fun getSecretValues(): String { @@ -85,7 +61,6 @@ class EMRTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/emr/src/test/resources/logback.xml b/kotlin/services/emr/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/emr/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/eventbridge/build.gradle.kts b/kotlin/services/eventbridge/build.gradle.kts index 07286ad0d75..b5a4f8712c4 100644 --- a/kotlin/services/eventbridge/build.gradle.kts +++ b/kotlin/services/eventbridge/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/eventbridge/src/test/kotlin/EventBridgeKotlinTest.kt b/kotlin/services/eventbridge/src/test/kotlin/EventBridgeKotlinTest.kt index 5963c275848..22d2656a729 100644 --- a/kotlin/services/eventbridge/src/test/kotlin/EventBridgeKotlinTest.kt +++ b/kotlin/services/eventbridge/src/test/kotlin/EventBridgeKotlinTest.kt @@ -12,10 +12,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class EventBridgeKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(EventBridgeKotlinTest::class.java) private var roleNameSc = "" private var bucketNameSc = "" private var topicNameSc = "" @@ -28,6 +31,6 @@ class EventBridgeKotlinTest { fun helloEventBridgeTest() = runBlocking { listBusesHello() - println("Test 1 passed") + logger.info("Test 1 passed") } } diff --git a/kotlin/services/eventbridge/src/test/resources/logback.xml b/kotlin/services/eventbridge/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/eventbridge/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/firehose/build.gradle.kts b/kotlin/services/firehose/build.gradle.kts index 8c70d22d4be..74a5cd33eed 100644 --- a/kotlin/services/firehose/build.gradle.kts +++ b/kotlin/services/firehose/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") implementation("com.google.code.gson:gson:2.10") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/firehose/src/test/kotlin/FirehoseTest.kt b/kotlin/services/firehose/src/test/kotlin/FirehoseTest.kt index f02ba3e9864..d5b729794c2 100644 --- a/kotlin/services/firehose/src/test/kotlin/FirehoseTest.kt +++ b/kotlin/services/firehose/src/test/kotlin/FirehoseTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -19,12 +18,15 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID import java.util.concurrent.TimeUnit @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class FirehoseTest { + private val logger: Logger = LoggerFactory.getLogger(FirehoseTest::class.java) private var bucketARN = "" private var roleARN = "" private var newStream = "" @@ -42,63 +44,49 @@ class FirehoseTest { roleARN = values.roleARN.toString() newStream = values.newStream.toString() + UUID.randomUUID() textValue = values.textValue.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - bucketARN = prop.getProperty("bucketARN") - roleARN = prop.getProperty("roleARN") - newStream = prop.getProperty("newStream") - textValue = prop.getProperty("textValue") - existingStream = prop.getProperty("existingStream") - delStream = prop.getProperty("delStream") - */ } @Test - @Order(2) + @Order(1) fun createDeliveryStreamTest() = runBlocking { createStream(bucketARN, roleARN, newStream) - println("Test 2 passed") + logger.info("Test 1 passed") } @Test - @Order(3) + @Order(2) fun putRecordsTest() = runBlocking { // Wait for the resource to become available println("Wait 15 mins for resource to become available.") TimeUnit.MINUTES.sleep(15) putSingleRecord(textValue, newStream) - println("Test 3 passed") + logger.info("Test 2 passed") } @Test - @Order(4) + @Order(3) fun putBatchRecordsTest() = runBlocking { addStockTradeData(newStream) - println("Test 4 passed") + logger.info("Test 3 passed") } @Test - @Order(5) + @Order(4) fun listDeliveryStreamsTest() = runBlocking { listStreams() - println("Test 5 passed") + logger.info("Test 4 passed") } @Test - @Order(6) + @Order(5) fun deleteStreamTest() = runBlocking { delStream(newStream) - println("Test 6 passed") + logger.info("Test 5 passed") } private suspend fun getSecretValues(): String { @@ -109,7 +97,6 @@ class FirehoseTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/firehose/src/test/resources/logback.xml b/kotlin/services/firehose/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/firehose/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/forecast/build.gradle.kts b/kotlin/services/forecast/build.gradle.kts index 41c296a2c09..ac766827fb1 100644 --- a/kotlin/services/forecast/build.gradle.kts +++ b/kotlin/services/forecast/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/forecast/src/test/kotlin/ForecastKotlinTest.kt b/kotlin/services/forecast/src/test/kotlin/ForecastKotlinTest.kt index 9c6cd44baf1..5444bb23702 100644 --- a/kotlin/services/forecast/src/test/kotlin/ForecastKotlinTest.kt +++ b/kotlin/services/forecast/src/test/kotlin/ForecastKotlinTest.kt @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -22,12 +21,15 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random import java.util.concurrent.TimeUnit @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class ForecastKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(ForecastKotlinTest::class.java) private var predictorARN = "" private var forecastArn = "" // set in test 3 private var forecastName = "" @@ -46,17 +48,6 @@ class ForecastKotlinTest { predictorARN = values.predARN.toString() forecastName = values.forecastName.toString() + randomNum dataSetName = values.dataSet.toString() + randomNum - - /* - // load the properties file. - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - forecastName = prop.getProperty("forecastName") - dataSetName = prop.getProperty("dataSetName") - predictorARN = prop.getProperty("predictorARN") - existingforecastDelete = prop.getProperty("existingforecastDelete") - */ } @Test @@ -65,7 +56,7 @@ class ForecastKotlinTest { runBlocking { myDataSetARN = createForecastDataSet(dataSetName).toString() assertTrue(!myDataSetARN.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -74,7 +65,7 @@ class ForecastKotlinTest { runBlocking { forecastArn = createNewForecast(forecastName, predictorARN).toString() assertTrue(!forecastArn.isEmpty()) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -82,7 +73,7 @@ class ForecastKotlinTest { fun listDataSets() = runBlocking { listForecastDataSets() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -90,7 +81,7 @@ class ForecastKotlinTest { fun listDataSetGroups() = runBlocking { listDataGroups() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -98,7 +89,7 @@ class ForecastKotlinTest { fun listForecasts() = runBlocking { listAllForeCasts() - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -106,7 +97,7 @@ class ForecastKotlinTest { fun describeForecast() = runBlocking { describe(forecastArn) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -114,7 +105,7 @@ class ForecastKotlinTest { fun deleteDataSet() = runBlocking { deleteForecastDataSet(myDataSetARN) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -124,7 +115,7 @@ class ForecastKotlinTest { println("Wait 40 mins for resource to become available.") TimeUnit.MINUTES.sleep(40) delForecast(forecastArn) - println("Test 8 passed") + logger.info("Test 8 passed") } private suspend fun getSecretValues(): String { @@ -135,7 +126,6 @@ class ForecastKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/forecast/src/test/resources/logback.xml b/kotlin/services/forecast/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/forecast/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/glue/build.gradle.kts b/kotlin/services/glue/build.gradle.kts index 6b562da1edb..45872e73ad0 100644 --- a/kotlin/services/glue/build.gradle.kts +++ b/kotlin/services/glue/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/glue/src/test/kotlin/GlueTest.kt b/kotlin/services/glue/src/test/kotlin/GlueTest.kt index 1dfc4b10781..e23b1ea69ef 100644 --- a/kotlin/services/glue/src/test/kotlin/GlueTest.kt +++ b/kotlin/services/glue/src/test/kotlin/GlueTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -18,11 +17,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class GlueTest { + private val logger: Logger = LoggerFactory.getLogger(GlueTest::class.java) private var cron = "" private var iam = "" private var tableName = "" @@ -53,23 +55,6 @@ class GlueTest { crawlerNameSc = values.crawlerNameSc.toString() + UUID.randomUUID() scriptLocationSc = values.scriptLocationSc.toString() locationUri = values.locationUri.toString() - - // Uncomment the block below if using config.properties file - /* - val input = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - cron = prop.getProperty("cron") - iam = prop.getProperty("IAM") - tableName = prop.getProperty("tableName") - text = prop.getProperty("text") - jobNameSc = prop.getProperty("jobNameSc") - s3PathSc = prop.getProperty("s3PathSc") - dbNameSc = prop.getProperty("dbNameSc") - crawlerNameSc = prop.getProperty("crawlerNameSc") - scriptLocationSc = prop.getProperty("scriptLocationSc") - locationUri = prop.getProperty("locationUri") - */ } @Test @@ -77,7 +62,7 @@ class GlueTest { fun getCrawlersTest() = runBlocking { getAllCrawlers() - println("Test 2 passed") + logger.info("Test 1 passed") } @Test @@ -85,7 +70,7 @@ class GlueTest { fun getDatabasesTest() = runBlocking { getAllDatabases() - println("Test 4 passed") + logger.info("Test 2 passed") } @Test @@ -93,7 +78,7 @@ class GlueTest { fun searchTablesTest() = runBlocking { searchGlueTable(text) - println("Test 5 passed") + logger.info("Test 3 passed") } @Test @@ -101,7 +86,7 @@ class GlueTest { fun listWorkflowsTest() = runBlocking { listAllWorkflows() - println("Test 6 passed") + logger.info("Test 4 passed") } private suspend fun getSecretValues(): String { @@ -112,7 +97,6 @@ class GlueTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/glue/src/test/resources/logback.xml b/kotlin/services/glue/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/glue/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/iam/build.gradle.kts b/kotlin/services/iam/build.gradle.kts index 40ba2d14ed2..44a71ed4570 100644 --- a/kotlin/services/iam/build.gradle.kts +++ b/kotlin/services/iam/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { implementation("com.googlecode.json-simple:json-simple:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/iam/src/test/kotlin/IAMTest.kt b/kotlin/services/iam/src/test/kotlin/IAMTest.kt index c755fb05b04..3902813a8f1 100644 --- a/kotlin/services/iam/src/test/kotlin/IAMTest.kt +++ b/kotlin/services/iam/src/test/kotlin/IAMTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -28,11 +27,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import kotlin.random.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class IAMTest { + private val logger: Logger = LoggerFactory.getLogger(IAMTest::class.java) private var userName = "" private var policyName = "" private var roleName = "" @@ -64,22 +66,6 @@ class IAMTest { roleSessionName = values.roleName.toString() fileLocationSc = values.fileLocationSc.toString() bucketNameSc = values.bucketNameSc.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - userName = prop.getProperty("userName") - policyName = prop.getProperty("policyName") - roleName = prop.getProperty("roleName") - accountAlias = prop.getProperty("accountAlias") - policyNameSc = prop.getProperty("policyNameSc") - usernameSc = prop.getProperty("usernameSc") - roleNameSc = prop.getProperty("roleNameSc") - roleSessionName = prop.getProperty("roleSessionName") - fileLocationSc = prop.getProperty("fileLocationSc") - bucketNameSc = prop.getProperty("bucketNameSc") - */ } @Test @@ -90,7 +76,7 @@ class IAMTest { if (result != null) { Assertions.assertTrue(!result.isEmpty()) } - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -99,7 +85,7 @@ class IAMTest { runBlocking { policyARN = createIAMPolicy(policyName) Assertions.assertTrue(!policyARN.isEmpty()) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -108,7 +94,7 @@ class IAMTest { runBlocking { keyId = createIAMAccessKey(userName) Assertions.assertTrue(!keyId.isEmpty()) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -116,7 +102,7 @@ class IAMTest { fun attachRolePolicyTest() = runBlocking { attachIAMRolePolicy(roleName, policyARN) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -124,7 +110,7 @@ class IAMTest { fun detachRolePolicyTest() = runBlocking { detachPolicy(roleName, policyARN) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -132,7 +118,7 @@ class IAMTest { fun getPolicyTest() = runBlocking { getIAMPolicy(policyARN) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -140,7 +126,7 @@ class IAMTest { fun listAccessKeysTest() = runBlocking { listKeys(userName) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -148,7 +134,7 @@ class IAMTest { fun listUsersTest() = runBlocking { listAllUsers() - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -156,7 +142,7 @@ class IAMTest { fun createAccountAliasTest() = runBlocking { createIAMAccountAlias(accountAlias) - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -164,7 +150,7 @@ class IAMTest { fun deleteAccountAliasTest() = runBlocking { deleteIAMAccountAlias(accountAlias) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -172,7 +158,7 @@ class IAMTest { fun deletePolicyTest() = runBlocking { deleteIAMPolicy(policyARN) - println("Test 11 passed") + logger.info("Test 11 passed") } @Test @@ -180,7 +166,7 @@ class IAMTest { fun deleteAccessKeyTest() = runBlocking { deleteKey(userName, keyId) - println("Test 12 passed") + logger.info("Test 12 passed") } @Test @@ -188,7 +174,7 @@ class IAMTest { fun deleteUserTest() = runBlocking { deleteIAMUser(userName) - println("Test 13 passed") + logger.info("Test 13 passed") } private suspend fun getSecretValues(): String { @@ -199,7 +185,6 @@ class IAMTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/iam/src/test/resources/logback.xml b/kotlin/services/iam/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/iam/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/iot/build.gradle.kts b/kotlin/services/iot/build.gradle.kts index 7f1c544d2b0..46c0cc459e9 100644 --- a/kotlin/services/iot/build.gradle.kts +++ b/kotlin/services/iot/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/iot/src/test/kotlin/IoTTest.kt b/kotlin/services/iot/src/test/kotlin/IoTTest.kt index 7af9fc520a8..3eb243fc7e0 100644 --- a/kotlin/services/iot/src/test/kotlin/IoTTest.kt +++ b/kotlin/services/iot/src/test/kotlin/IoTTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.example.iot.attachCertificateToThing @@ -30,11 +29,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class IoTTest { + private val logger: Logger = LoggerFactory.getLogger(IoTTest::class.java) private var roleARN = "" private var snsAction = "" private var thingName = "foo" @@ -63,7 +65,7 @@ class IoTTest { fun helloIoTTest() = runBlocking { listAllThings() - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -89,14 +91,13 @@ class IoTTest { deleteCertificate(certificateArn) } deleteIoTThing(thingName) - println("Test 2 passed") + logger.info("Test 2 passed") } private suspend fun getSecretValues(): String { val secretClient = SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() } val secretName = "test/iot" val valueRequest = diff --git a/kotlin/services/iot/src/test/resources/logback.xml b/kotlin/services/iot/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/iot/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/kendra/build.gradle.kts b/kotlin/services/kendra/build.gradle.kts index 6f8ea8043f3..6588a3b090c 100644 --- a/kotlin/services/kendra/build.gradle.kts +++ b/kotlin/services/kendra/build.gradle.kts @@ -31,12 +31,13 @@ dependencies { implementation(platform("aws.sdk.kotlin:bom:1.3.112")) implementation("aws.sdk.kotlin:kendra") implementation("aws.sdk.kotlin:secretsmanager") - testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("aws.smithy.kotlin:http-client-engine-okhttp") implementation("aws.smithy.kotlin:http-client-engine-crt") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - implementation("com.google.code.gson:gson:2.10") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/kendra/src/test/kotlin/KendraTest.kt b/kotlin/services/kendra/src/test/kotlin/KendraTest.kt index fef6b0c40aa..0129c724d2d 100644 --- a/kotlin/services/kendra/src/test/kotlin/KendraTest.kt +++ b/kotlin/services/kendra/src/test/kotlin/KendraTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.kendra.KendraClient import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest @@ -23,11 +22,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class KendraTest { + private val logger: Logger = LoggerFactory.getLogger(KendraTest::class.java) private var kendra: KendraClient? = null private var indexName = "" private var indexDescription = "" @@ -55,29 +57,6 @@ class KendraTest { dataSourceDescription = values.dataSourceDescription.toString() dataSourceRoleArn = values.dataSourceRoleArn.toString() text = values.text.toString() - - /* - try { - KendraTest::class.java.classLoader.getResourceAsStream("config.properties").use { input -> - val prop = Properties() - if (input == null) { - println("Sorry, unable to find config.properties") - return - } - prop.load(input) - indexName = prop.getProperty("indexName") - indexRoleArn = prop.getProperty("indexRoleArn") - indexDescription = prop.getProperty("indexDescription") - s3BucketName = prop.getProperty("s3BucketName") - dataSourceName = prop.getProperty("dataSourceName") - dataSourceDescription = prop.getProperty("dataSourceDescription") - dataSourceRoleArn = prop.getProperty("dataSourceRoleArn") - text = prop.getProperty("text") - } - } catch (ex: IOException) { - ex.printStackTrace() - } - */ } @Test @@ -86,7 +65,7 @@ class KendraTest { runBlocking { indexId = createIndex(indexDescription, indexName, indexRoleArn) assertTrue(!indexId.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -95,7 +74,7 @@ class KendraTest { runBlocking { dataSourceId = createDataSource(s3BucketName, dataSourceName, dataSourceDescription, indexId, dataSourceRoleArn) assertTrue(!dataSourceId.isEmpty()) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -103,7 +82,7 @@ class KendraTest { fun syncDataSource() = runBlocking { startDataSource(indexId, dataSourceId) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -111,7 +90,7 @@ class KendraTest { fun listSyncJobs() = runBlocking { listSyncJobs(indexId, dataSourceId) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -119,7 +98,7 @@ class KendraTest { fun queryIndex() = runBlocking { querySpecificIndex(indexId, text) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -127,7 +106,7 @@ class KendraTest { fun deleteDataSource() = runBlocking { deleteSpecificDataSource(indexId, dataSourceId) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -135,7 +114,7 @@ class KendraTest { fun deleteIndex() = runBlocking { deleteSpecificIndex(indexId) - println("Test 7 passed") + logger.info("Test 7 passed") } private suspend fun getSecretValues(): String { @@ -146,7 +125,6 @@ class KendraTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/kendra/src/test/resources/logback.xml b/kotlin/services/kendra/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/kendra/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/keyspaces/build.gradle.kts b/kotlin/services/keyspaces/build.gradle.kts index 01946318370..65024ef6694 100644 --- a/kotlin/services/keyspaces/build.gradle.kts +++ b/kotlin/services/keyspaces/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-core:2.14.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/keyspaces/src/test/kotlin/KeyspaceTest.kt b/kotlin/services/keyspaces/src/test/kotlin/KeyspaceTest.kt index fc4a4047bd0..d1e63b7014e 100644 --- a/kotlin/services/keyspaces/src/test/kotlin/KeyspaceTest.kt +++ b/kotlin/services/keyspaces/src/test/kotlin/KeyspaceTest.kt @@ -8,14 +8,19 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class KeyspaceTest { + private val logger: Logger = LoggerFactory.getLogger(KeyspaceTest::class.java) + @Test @Order(1) fun keyspaceTest() = runBlocking { listKeyspaces() + logger.info("Test 1 passed") } } diff --git a/kotlin/services/keyspaces/src/test/resources/logback.xml b/kotlin/services/keyspaces/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/keyspaces/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/kinesis/build.gradle.kts b/kotlin/services/kinesis/build.gradle.kts index cfa3a38a304..cacf949064a 100644 --- a/kotlin/services/kinesis/build.gradle.kts +++ b/kotlin/services/kinesis/build.gradle.kts @@ -34,6 +34,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.0") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/kinesis/src/test/kotlin/KinesisTest.kt b/kotlin/services/kinesis/src/test/kotlin/KinesisTest.kt index bd879dcb8e0..4159261c990 100644 --- a/kotlin/services/kinesis/src/test/kotlin/KinesisTest.kt +++ b/kotlin/services/kinesis/src/test/kotlin/KinesisTest.kt @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random import java.util.concurrent.TimeUnit import kotlin.system.exitProcess @@ -19,6 +21,7 @@ import kotlin.system.exitProcess @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class KinesisTest { + private val logger: Logger = LoggerFactory.getLogger(KinesisTest::class.java) private var streamName = "Stream" @BeforeAll @@ -33,7 +36,7 @@ class KinesisTest { fun createDataStreamTest() = runBlocking { createStream(streamName) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -41,7 +44,7 @@ class KinesisTest { fun describeLimitsTest() = runBlocking { describeKinLimits() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -56,14 +59,14 @@ class KinesisTest { System.err.println(e.message) exitProcess(1) } - println("Test 4 passed") + logger.info("Test 3 passed") } @Test - @Order(5) + @Order(4) fun deleteDataStreamTest() = runBlocking { deleteStream(streamName) - println("Test 7 passed") + logger.info("Test 4 passed") } } diff --git a/kotlin/services/kinesis/src/test/resources/logback.xml b/kotlin/services/kinesis/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/kinesis/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/kms/build.gradle.kts b/kotlin/services/kms/build.gradle.kts index da517edbbec..3834c0fa08d 100644 --- a/kotlin/services/kms/build.gradle.kts +++ b/kotlin/services/kms/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt b/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt index 30a2b04b91d..739022bfa8c 100644 --- a/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt +++ b/kotlin/services/kms/src/test/kotlin/KMSKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -28,10 +27,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class KMSKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(KMSKotlinTest::class.java) private var keyId = "" // gets set in test 2 private var keyDesc = "" private var granteePrincipal = "" @@ -50,76 +52,66 @@ class KMSKotlinTest { operation = values.operation.toString() aliasName = values.aliasName.toString() granteePrincipal = values.granteePrincipal.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - keyDesc = prop.getProperty("keyDesc") - granteePrincipal = prop.getProperty("granteePrincipal") - operation = prop.getProperty("operation") - aliasName = prop.getProperty("aliasName") - */ } @Test - @Order(2) + @Order(1) fun createCustomerKeyTest() = runBlocking { keyId = createKey(keyDesc).toString() Assertions.assertTrue(!keyId.isEmpty()) - println("Test 2 passed") + logger.info("Test 1 passed") } @Test - @Order(3) + @Order(2) fun encryptDataKeyTest() = runBlocking { val plaintext = "Hello, AWS KMS!" val encryptData = encryptData(keyId) decryptData(encryptData, keyId) - println("Test 3 passed") + logger.info("Test 2 passed") } @Test - @Order(4) + @Order(3) fun disableCustomerKeyTest() = runBlocking { disableKey(keyId) - println("Test 4 passed") + logger.info("Test 3 passed") } @Test - @Order(5) + @Order(4) fun enableCustomerKeyTest() = runBlocking { enableKey(keyId) - println("Test 5 passed") + logger.info("Test 4 passed") } @Test - @Order(6) + @Order(5) fun createGrantTest() = runBlocking { grantId = createNewGrant(keyId, granteePrincipal, operation).toString() Assertions.assertTrue(!grantId.isEmpty()) - println("Test 6 passed") + logger.info("Test 5 passed") } @Test - @Order(7) + @Order(6) fun listGrantsTest() = runBlocking { displayGrantIds(keyId) - println("Test 7 passed") + logger.info("Test 6 passed") } @Test - @Order(8) + @Order(7) fun revokeGrantsTest() = runBlocking { revokeKeyGrant(keyId, grantId) - println("Test 8 passed") + logger.info("Test 7 passed") } @Test @@ -127,39 +119,39 @@ class KMSKotlinTest { fun describeKeyTest() = runBlocking { describeSpecifcKey(keyId) - println("Test 9 passed") + logger.info("Test 8 passed") } @Test - @Order(10) + @Order(9) fun createAliasTest() = runBlocking { createCustomAlias(keyId, aliasName) - println("Test 10 passed") + logger.info("Test 9 passed") } @Test - @Order(11) + @Order(10) fun listAliasesTest() = runBlocking { listAllAliases() - println("Test 11 passed") + logger.info("Test 10 passed") } @Test - @Order(12) + @Order(11) fun deleteAliasTest() = runBlocking { deleteSpecificAlias(aliasName) - println("Test 12 passed") + logger.info("Test 11 passed") } @Test - @Order(13) + @Order(12) fun listKeysTest() = runBlocking { listAllKeys() - println("Test 13 passed") + logger.info("Test 12 passed") } private suspend fun getSecretValues(): String { @@ -170,7 +162,6 @@ class KMSKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/kms/src/test/resources/logback.xml b/kotlin/services/kms/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/kms/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/lambda/build.gradle.kts b/kotlin/services/lambda/build.gradle.kts index 914003faa52..da62888bee8 100644 --- a/kotlin/services/lambda/build.gradle.kts +++ b/kotlin/services/lambda/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt b/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt index e873d41fd4d..fa481f068b9 100644 --- a/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt +++ b/kotlin/services/lambda/src/test/kotlin/LambdaTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -26,11 +25,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class LambdaTest { + private val logger: Logger = LoggerFactory.getLogger(LambdaTest::class.java) var functionName: String = "" var functionARN: String = "" // Gets set in a test. var s3BucketName: String = "" @@ -55,19 +57,6 @@ class LambdaTest { s3BucketName = values.bucketName.toString() updatedBucketName = values.bucketName2.toString() s3Key = values.key.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - functionName = prop.getProperty("functionName") - functionNameSc = prop.getProperty("functionNameSc") - s3BucketName = prop.getProperty("s3BucketName") - updatedBucketName = prop.getProperty("updatedBucketName") - s3Key = prop.getProperty("s3Key") - role = prop.getProperty("role") - handler = prop.getProperty("handler") - */ } @Test @@ -76,7 +65,7 @@ class LambdaTest { runBlocking { functionARN = createNewFunction(functionName, s3BucketName, s3Key, handler, role).toString() Assertions.assertTrue(!functionARN.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -84,7 +73,7 @@ class LambdaTest { fun listLambdaTest() = runBlocking { listFunctions() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -92,7 +81,7 @@ class LambdaTest { fun getAccountSettings() = runBlocking { getSettings() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -100,7 +89,7 @@ class LambdaTest { fun deleteFunctionTest() = runBlocking { delLambdaFunction(functionName) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -137,6 +126,7 @@ class LambdaTest { // Delete the AWS Lambda function. println("Delete the AWS Lambda function.") delFunction(functionNameSc) + logger.info("Test 5 passed") } private suspend fun getSecretValues(): String { @@ -147,7 +137,6 @@ class LambdaTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/lambda/src/test/resources/logback.xml b/kotlin/services/lambda/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/lambda/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/lex/build.gradle.kts b/kotlin/services/lex/build.gradle.kts index 79c09d0ed5f..a5caa6e3501 100644 --- a/kotlin/services/lex/build.gradle.kts +++ b/kotlin/services/lex/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/lex/src/test/kotlin/LexTest.kt b/kotlin/services/lex/src/test/kotlin/LexTest.kt index 9629e4d1e24..c76d853eea4 100644 --- a/kotlin/services/lex/src/test/kotlin/LexTest.kt +++ b/kotlin/services/lex/src/test/kotlin/LexTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -20,10 +19,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class LexTest { + private val logger: Logger = LoggerFactory.getLogger(LexTest::class.java) private var botName = "" private var intentName = "" private var intentVersion = "" @@ -38,13 +40,6 @@ class LexTest { botName = values.botName.toString() intentName = values.intentName.toString() intentVersion = values.intentVersion.toString() - - // val input = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - // val prop = Properties() - // prop.load(input) - // botName = prop.getProperty("botName") - // intentName = prop.getProperty("intentName") - // intentVersion = prop.getProperty("intentVersion") } @Test @@ -52,7 +47,7 @@ class LexTest { fun putBotTest() = runBlocking { createBot(botName, intentName, intentVersion) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -60,7 +55,7 @@ class LexTest { fun getBotsTest() = runBlocking { getAllBots() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -68,7 +63,7 @@ class LexTest { fun getIntentTest() = runBlocking { getSpecificIntent(intentName, intentVersion) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -76,7 +71,7 @@ class LexTest { fun getSlotTypesTest() = runBlocking { getSlotsInfo() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -84,7 +79,7 @@ class LexTest { fun getBotStatusTest() = runBlocking { getStatus(botName) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -92,7 +87,7 @@ class LexTest { fun deleteBotTest() = runBlocking { deleteSpecificBot(botName) - println("Test 6 passed") + logger.info("Test 6 passed") } private suspend fun getSecretValues(): String { @@ -103,7 +98,6 @@ class LexTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/lex/src/test/resources/logback.xml b/kotlin/services/lex/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/lex/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/mediaconvert/build.gradle.kts b/kotlin/services/mediaconvert/build.gradle.kts index 8c01f96a3eb..680aa4db3ef 100644 --- a/kotlin/services/mediaconvert/build.gradle.kts +++ b/kotlin/services/mediaconvert/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt b/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt index 8c1cc99a7d9..a278903e946 100644 --- a/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt +++ b/kotlin/services/mediaconvert/src/test/kotlin/MCTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.mediaconvert.MediaConvertClient import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest @@ -19,11 +18,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.io.IOException @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class MCTest { + private val logger: Logger = LoggerFactory.getLogger(MCTest::class.java) lateinit var mcClient: MediaConvertClient private var mcRoleARN = "" private var fileInput = "" @@ -40,39 +42,31 @@ class MCTest { val values = gson.fromJson(json, SecretValues::class.java) mcRoleARN = values.mcRoleARN.toString() fileInput = values.fileInput.toString() - /* - - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - mcRoleARN = prop.getProperty("mcRoleARN") - fileInput = prop.getProperty("fileInput") - */ } @Test - @Order(2) + @Order(1) fun createJobTest() = runBlocking { jobId = createMediaJob(mcClient, mcRoleARN, fileInput).toString() assertTrue(!jobId.isEmpty()).toString() - println("Test 2 passed") + logger.info("Test 1 passed") } @Test - @Order(3) + @Order(2) fun listJobsTest() = runBlocking { listCompleteJobs(mcClient) - println("Test 3 passed") + logger.info("Test 2 passed") } @Test - @Order(4) + @Order(3) fun getJobTest() = runBlocking { getSpecificJob(mcClient, jobId) - println("Test 4 passed") + logger.info("Test 3 passed") } private suspend fun getSecretValues(): String { @@ -83,7 +77,6 @@ class MCTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/mediaconvert/src/test/resources/logback.xml b/kotlin/services/mediaconvert/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/mediaconvert/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/mediastore/build.gradle.kts b/kotlin/services/mediastore/build.gradle.kts index dc7ca982e4c..8965d5f82bb 100644 --- a/kotlin/services/mediastore/build.gradle.kts +++ b/kotlin/services/mediastore/build.gradle.kts @@ -32,6 +32,8 @@ dependencies { implementation("aws.smithy.kotlin:http-client-engine-crt") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/mediastore/src/test/kotlin/MediaStoreTest.kt b/kotlin/services/mediastore/src/test/kotlin/MediaStoreTest.kt index c4a6c4dc0e2..4aa0cdfbe97 100644 --- a/kotlin/services/mediastore/src/test/kotlin/MediaStoreTest.kt +++ b/kotlin/services/mediastore/src/test/kotlin/MediaStoreTest.kt @@ -12,11 +12,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class MediaStoreTest { + private val logger: Logger = LoggerFactory.getLogger(MediaStoreTest::class.java) private var containerName = "" @BeforeAll @@ -30,16 +33,16 @@ class MediaStoreTest { @Order(1) fun createContainerTest() = runBlocking { - println("Status is " + createMediaContainer(containerName)) - println("Test 1 passed") + logger.info("Status is " + createMediaContainer(containerName)) + logger.info("Test 1 passed") } @Test @Order(2) fun describeContainerTest() = runBlocking { - println("Status is " + checkContainer(containerName)) - println("Test 2 passed") + logger.info("Status is " + checkContainer(containerName)) + logger.info("Test 2 passed") } @Test @@ -47,7 +50,7 @@ class MediaStoreTest { fun listContainersTest() = runBlocking { listAllContainers() - println("Test 4 passed") + logger.info("Test 3 passed") } @Test @@ -55,6 +58,6 @@ class MediaStoreTest { fun deleteContainerTest() = runBlocking { deleteMediaContainer(containerName) - println("Test 4 passed") + logger.info("Test 4 passed") } } diff --git a/kotlin/services/mediastore/src/test/resources/logback.xml b/kotlin/services/mediastore/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/mediastore/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/opensearch/build.gradle.kts b/kotlin/services/opensearch/build.gradle.kts index 74e58d61552..43f14cae347 100644 --- a/kotlin/services/opensearch/build.gradle.kts +++ b/kotlin/services/opensearch/build.gradle.kts @@ -34,6 +34,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { diff --git a/kotlin/services/opensearch/src/test/kotlin/OpenSearchTest.kt b/kotlin/services/opensearch/src/test/kotlin/OpenSearchTest.kt index c763f8d36a0..eee5fa8fd54 100644 --- a/kotlin/services/opensearch/src/test/kotlin/OpenSearchTest.kt +++ b/kotlin/services/opensearch/src/test/kotlin/OpenSearchTest.kt @@ -11,11 +11,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class OpenSearchTest { + private val logger: Logger = LoggerFactory.getLogger(OpenSearchTest::class.java) private var domainName = "" @BeforeAll @@ -30,7 +33,7 @@ class OpenSearchTest { fun createDomainTest() = runBlocking { createNewDomain(domainName) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -38,7 +41,7 @@ class OpenSearchTest { fun listDomainNamesTest() = runBlocking { listAllDomains() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -46,6 +49,6 @@ class OpenSearchTest { fun deleteDomainTest() = runBlocking { deleteSpecificDomain(domainName) - println("Test 4 passed") + logger.info("Test 3 passed") } } diff --git a/kotlin/services/opensearch/src/test/resources/logback.xml b/kotlin/services/opensearch/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/opensearch/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/personalize/build.gradle.kts b/kotlin/services/personalize/build.gradle.kts index e6db9de0963..6c4e3754f42 100644 --- a/kotlin/services/personalize/build.gradle.kts +++ b/kotlin/services/personalize/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/personalize/src/test/kotlin/PersonalizeKotlinTest.kt b/kotlin/services/personalize/src/test/kotlin/PersonalizeKotlinTest.kt index 63a43976124..c217663285d 100644 --- a/kotlin/services/personalize/src/test/kotlin/PersonalizeKotlinTest.kt +++ b/kotlin/services/personalize/src/test/kotlin/PersonalizeKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -26,12 +25,15 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID import java.util.concurrent.TimeUnit @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class PersonalizeKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(PersonalizeKotlinTest::class.java) private var datasetGroupArn = "" private var solutionArn = "" private var solutionVersionArn = "" @@ -55,18 +57,6 @@ class PersonalizeKotlinTest { solutionName = values.solutionName.toString() + UUID.randomUUID() userId = values.userId.toString() campaignName = values.campaignName.toString() + UUID.randomUUID() - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - solutionName = prop.getProperty("solutionName") - datasetGroupArn = prop.getProperty("datasetGroupArn") - recipeArn = prop.getProperty("recipeArn") - solutionVersionArn = prop.getProperty("solutionVersionArn") - campaignName = prop.getProperty("campaignName") - campaignArn = prop.getProperty("campaignArn") - userId = prop.getProperty("userId") - */ } @Test @@ -75,7 +65,7 @@ class PersonalizeKotlinTest { runBlocking { solutionArn = createPersonalizeSolution(datasetGroupArn, solutionName, recipeArn).toString() assertTrue(!solutionArn.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -83,7 +73,7 @@ class PersonalizeKotlinTest { fun listSolutions() = runBlocking { listAllSolutions(datasetGroupArn) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -91,7 +81,7 @@ class PersonalizeKotlinTest { fun describeSolution() = runBlocking { describeSpecificSolution(solutionArn) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -100,7 +90,7 @@ class PersonalizeKotlinTest { runBlocking { newCampaignArn = createPersonalCompaign(solutionVersionArn, campaignName).toString() assertTrue(!newCampaignArn.isEmpty()) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -110,7 +100,7 @@ class PersonalizeKotlinTest { println("Wait 20 mins for resource to become available.") TimeUnit.MINUTES.sleep(20) describeSpecificCampaign(newCampaignArn) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -118,7 +108,7 @@ class PersonalizeKotlinTest { fun listCampaigns() = runBlocking { listAllCampaigns(solutionArn) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -126,7 +116,7 @@ class PersonalizeKotlinTest { fun listRecipes() = runBlocking { listAllRecipes() - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -134,7 +124,7 @@ class PersonalizeKotlinTest { fun listDatasetGroups() = runBlocking { listDSGroups() - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -142,7 +132,7 @@ class PersonalizeKotlinTest { fun deleteSolution() = runBlocking { deleteGivenSolution(solutionArn) - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -150,7 +140,7 @@ class PersonalizeKotlinTest { fun getRecommendations() = runBlocking { getRecs(newCampaignArn, userId) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -158,7 +148,7 @@ class PersonalizeKotlinTest { fun deleteCampaign() = runBlocking { deleteSpecificCampaign(newCampaignArn) - println("Test 11 passed") + logger.info("Test 11 passed") } private suspend fun getSecretValues(): String { @@ -169,7 +159,6 @@ class PersonalizeKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/personalize/src/test/resources/logback.xml b/kotlin/services/personalize/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/personalize/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/pinpoint/build.gradle.kts b/kotlin/services/pinpoint/build.gradle.kts index 17e1e8e54b2..86eee956a4b 100644 --- a/kotlin/services/pinpoint/build.gradle.kts +++ b/kotlin/services/pinpoint/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.google.code.gson:gson:2.10.1") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/pinpoint/src/test/kotlin/PinpointKotlinTest.kt b/kotlin/services/pinpoint/src/test/kotlin/PinpointKotlinTest.kt index 1242985e104..840faabb6af 100644 --- a/kotlin/services/pinpoint/src/test/kotlin/PinpointKotlinTest.kt +++ b/kotlin/services/pinpoint/src/test/kotlin/PinpointKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -27,10 +26,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class PinpointKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(PinpointKotlinTest::class.java) private var appName = "" private var appId = "" private var endpointId = "" @@ -60,28 +62,6 @@ class PinpointKotlinTest { toAddress = valuesOb.toAddress.toString() subject = valuesOb.subject.toString() existingApp = valuesOb.existingApplicationId.toString() - - /* - try { - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // load the properties file. - prop.load(input) - appName = prop.getProperty("appName") - originationNumber = prop.getProperty("originationNumber") - destinationNumber = prop.getProperty("destinationNumber") - message = prop.getProperty("message") - userId = prop.getProperty("userId") - senderAddress = prop.getProperty("senderAddress") - toAddress = prop.getProperty("toAddress") - subject = prop.getProperty("subject") - existingApp = prop.getProperty("existingApp") - existingEndpoint = prop.getProperty("existingEndpoint") - } catch (ex: IOException) { - ex.printStackTrace() - } - */ } @Test @@ -90,7 +70,7 @@ class PinpointKotlinTest { runBlocking { appId = createApplication(appName).toString() assertTrue(!appId.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -99,7 +79,7 @@ class PinpointKotlinTest { runBlocking { endpointId = createPinpointEndpoint(appId).toString() assertTrue(!endpointId.isEmpty()) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -107,7 +87,7 @@ class PinpointKotlinTest { fun addExampleEndpointTest() = runBlocking { updateEndpointsViaBatch(appId) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -115,7 +95,7 @@ class PinpointKotlinTest { fun lookUpEndpointTest() = runBlocking { lookupPinpointEndpoint(appId, endpointId) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -123,7 +103,7 @@ class PinpointKotlinTest { fun deleteEndpointTest() = runBlocking { deletePinEncpoint(appId, endpointId) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -131,7 +111,7 @@ class PinpointKotlinTest { fun sendMessageTest() = runBlocking { sendSMSMessage(message, appId, originationNumber, destinationNumber) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -140,7 +120,7 @@ class PinpointKotlinTest { runBlocking { segmentId = createPinpointSegment(appId).toString() assertTrue(!segmentId.isEmpty()) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -148,7 +128,7 @@ class PinpointKotlinTest { fun listSegmentsTest() = runBlocking { listSegs(appId) - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -156,7 +136,7 @@ class PinpointKotlinTest { fun createCampaignTest() = runBlocking { createPinCampaign(appId, segmentId) - println("Test 9 passed") + logger.info("Test 9 passed") } @Test @@ -164,7 +144,7 @@ class PinpointKotlinTest { fun sendEmailMessageTest() = runBlocking { sendEmail(subject, senderAddress, toAddress) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -172,7 +152,7 @@ class PinpointKotlinTest { fun listEndpointIdsTest() = runBlocking { listAllEndpoints(existingApp, userId) - println("Test 11 passed") + logger.info("Test 11 passed") } @Test @@ -180,7 +160,7 @@ class PinpointKotlinTest { fun deleteAppTest() = runBlocking { deletePinApp(appId) - println("Test 12 passed") + logger.info("Test 12 passed") } private suspend fun getSecretValues(): String { @@ -191,7 +171,6 @@ class PinpointKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/pinpoint/src/test/resources/logback.xml b/kotlin/services/pinpoint/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/pinpoint/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/polly/build.gradle.kts b/kotlin/services/polly/build.gradle.kts index a0e7d838ebd..9642b6b3e8d 100644 --- a/kotlin/services/polly/build.gradle.kts +++ b/kotlin/services/polly/build.gradle.kts @@ -34,6 +34,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.googlecode.soundlibs:jlayer:1.0.1.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/polly/src/test/kotlin/PollyKotlinTest.kt b/kotlin/services/polly/src/test/kotlin/PollyKotlinTest.kt index 8cd08e0c31a..c228e682a16 100644 --- a/kotlin/services/polly/src/test/kotlin/PollyKotlinTest.kt +++ b/kotlin/services/polly/src/test/kotlin/PollyKotlinTest.kt @@ -10,16 +10,20 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class PollyKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(PollyKotlinTest::class.java) + @Test @Order(1) fun pollyDemo() = runBlocking { talkPolly() - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -27,7 +31,7 @@ class PollyKotlinTest { fun describeVoicesSample() = runBlocking { describeVoice() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -35,6 +39,6 @@ class PollyKotlinTest { fun listLexiconsTest() = runBlocking { listLexicons() - println("Test 3 passed") + logger.info("Test 3 passed") } } diff --git a/kotlin/services/polly/src/test/resources/logback.xml b/kotlin/services/polly/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/polly/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/rds/build.gradle.kts b/kotlin/services/rds/build.gradle.kts index 8fa7b076a7c..6fba6c6c379 100644 --- a/kotlin/services/rds/build.gradle.kts +++ b/kotlin/services/rds/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/rds/src/test/kotlin/RDSTest.kt b/kotlin/services/rds/src/test/kotlin/RDSTest.kt index d7d6d7f7833..cd3a19101a9 100644 --- a/kotlin/services/rds/src/test/kotlin/RDSTest.kt +++ b/kotlin/services/rds/src/test/kotlin/RDSTest.kt @@ -32,6 +32,8 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random import java.util.UUID @@ -42,6 +44,7 @@ import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class RDSTest { + private val logger: Logger = LoggerFactory.getLogger(RDSTest::class.java) private var dbInstanceIdentifier = "" private var dbSnapshotIdentifier = "" private var dbName = "" @@ -75,116 +78,76 @@ class RDSTest { dbInstanceIdentifierSc = values.dbInstanceIdentifierSc + UUID.randomUUID() dbSnapshotIdentifierSc = values.dbSnapshotIdentifierSc + UUID.randomUUID() dbNameSc = values.dbNameSc + randomNum - -// Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // Load the properties file. - prop.load(input) - dbInstanceIdentifier = prop.getProperty("dbInstanceIdentifier") - dbSnapshotIdentifier = prop.getProperty("dbSnapshotIdentifier") - dbName = prop.getProperty("dbName") - masterUsername = prop.getProperty("masterUsername") - masterUserPassword = prop.getProperty("masterUserPassword") - newMasterUserPassword = prop.getProperty("newMasterUserPassword") - dbGroupNameSc = prop.getProperty("dbGroupNameSc") - dbParameterGroupFamilySc = prop.getProperty("dbParameterGroupFamilySc") - dbInstanceIdentifierSc = prop.getProperty("dbInstanceIdentifierSc") - masterUsernameSc = prop.getProperty("masterUsernameSc") - masterUserPasswordSc = prop.getProperty("masterUserPasswordSc") - dbSnapshotIdentifierSc = prop.getProperty("dbSnapshotIdentifierSc") - dbNameSc = prop.getProperty("dbNameSc") - */ } @Test - @Order(2) + @Order(1) fun createDBInstanceTest() = runBlocking { createDatabaseInstance(dbInstanceIdentifier, dbName, masterUsername, masterUserPassword) - println("Test 2 passed") + logger.info("Test 1 passed") } @Test - @Order(3) + @Order(2) fun waitForInstanceReadyTest() = runBlocking { waitForInstanceReady(dbInstanceIdentifier) - println("Test 3 passed") + logger.info("Test 2 passed") } @Test - @Order(4) + @Order(3) fun describeAccountAttributesTest() = runBlocking { getAccountAttributes() - println("Test 4 passed") + logger.info("Test 3 passed") } @Test - @Order(5) + @Order(4) fun describeDBInstancesTest() = runBlocking { describeInstances() - println("Test 5 passed") + logger.info("Test 4 passed") } @Test - @Order(6) + @Order(5) fun modifyDBInstanceTest() = runBlocking { updateIntance(dbInstanceIdentifier, newMasterUserPassword) - println("Test 6 passed") + logger.info("Test 5 passed") } @Test - @Order(7) + @Order(6) fun createDBSnapshotTest() = runBlocking { createSnapshot(dbInstanceIdentifier, dbSnapshotIdentifier) - println("Test 7 passed") + logger.info("Test 6 passed") } @Test - @Order(8) + @Order(7) fun deleteDBInstanceTest() = runBlocking { deleteDatabaseInstance(dbInstanceIdentifier) - println("Test 8 passed") + logger.info("Test 7 passed") } @Test - @Order(9) + @Order(8) fun scenarioTest() = runBlocking { - println("1. Return a list of the available DB engines") describeDBEngines() - - println("2. Create a custom parameter group") createDBParameterGroup(dbGroupNameSc, dbParameterGroupFamilySc) - - println("3. Get the parameter groups") describeDbParameterGroups(dbGroupNameSc) - - println("4. Get the parameters in the group") describeDbParameters(dbGroupNameSc, 0) - - println("5. Modify the auto_increment_offset parameter") modifyDBParas(dbGroupNameSc) - - println("6. Display the updated value") describeDbParameters(dbGroupNameSc, -1) - - println("7. Get a list of allowed engine versions") getAllowedEngines(dbParameterGroupFamilySc) - - println("8. Get a list of micro instance classes available for the selected engine") getMicroInstances() - - println("9. Create an RDS database instance that contains a MySql database and uses the parameter group") val dbARN = createDatabaseInstance( dbGroupNameSc, @@ -193,25 +156,14 @@ class RDSTest { masterUsername, masterUserPassword, ) - println("The ARN of the new database is $dbARN") - - println("10. Wait for DB instance to be ready") waitForDbInstanceReady(dbInstanceIdentifierSc) - - println("11. Create a snapshot of the DB instance") createDbSnapshot(dbInstanceIdentifierSc, dbSnapshotIdentifierSc) - - println("12. Wait for DB snapshot to be ready") waitForSnapshotReady(dbInstanceIdentifierSc, dbSnapshotIdentifierSc) - - println("13. Delete the DB instance") deleteDbInstance(dbInstanceIdentifierSc) - - println("14. Delete the parameter group") if (dbARN != null) { deleteParaGroup(dbGroupNameSc, dbARN) } - println("The Scenario has successfully completed.") + logger.info("Test 8 passed.") } suspend fun getSecretValues(): String? { diff --git a/kotlin/services/rds/src/test/resources/logback.xml b/kotlin/services/rds/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/rds/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/redshift/build.gradle.kts b/kotlin/services/redshift/build.gradle.kts index 0b8aa9aad99..92a2d915e89 100644 --- a/kotlin/services/redshift/build.gradle.kts +++ b/kotlin/services/redshift/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt b/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt index 782b85e2126..3034805b7ed 100644 --- a/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt +++ b/kotlin/services/redshift/src/test/kotlin/RedshiftKotlinTest.kt @@ -15,6 +15,8 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random /** @@ -24,6 +26,7 @@ import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class RedshiftKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(RedshiftKotlinTest::class.java) private var clusterId = "" private var eventSourceType = "" private var username = "" @@ -43,16 +46,6 @@ class RedshiftKotlinTest { username = values.userName.toString() password = values.password.toString() eventSourceType = values.eventSourceType.toString() - -// Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. -/* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - clusterId = prop.getProperty("clusterId") - eventSourceType = prop.getProperty("eventSourceType") - secretName prop.getProperty("secretName") - */ } @Test @@ -60,7 +53,7 @@ class RedshiftKotlinTest { fun createClusterTest() = runBlocking { createCluster(clusterId, username, password) - println("Test 2 passed") + logger.info("Test 1 passed") } @Test @@ -68,7 +61,7 @@ class RedshiftKotlinTest { fun describeClustersTest() = runBlocking { describeRedshiftClusters() - println("Test 5 passed") + logger.info("Test 2 passed") } @Test @@ -76,7 +69,7 @@ class RedshiftKotlinTest { fun findReservedNodeOfferTest() = runBlocking { findReservedNodeOffer() - println("Test 6 passed") + logger.info("Test 3 passed") } suspend fun getSecretValues(): String? { diff --git a/kotlin/services/redshift/src/test/resources/logback.xml b/kotlin/services/redshift/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/redshift/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/rekognition/build.gradle.kts b/kotlin/services/rekognition/build.gradle.kts index 0a39f0ec898..fb3c6dc7d0b 100644 --- a/kotlin/services/rekognition/build.gradle.kts +++ b/kotlin/services/rekognition/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectInappropriate.kt b/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectInappropriate.kt index 1d86649ab7d..e3f3c6096a7 100644 --- a/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectInappropriate.kt +++ b/kotlin/services/rekognition/src/main/kotlin/com/kotlin/rekognition/VideoDetectInappropriate.kt @@ -104,7 +104,7 @@ suspend fun getModResults() { while (!finished) { modDetectionResponse = rekClient.getContentModeration(modRequest) status = modDetectionResponse.jobStatus.toString() - if (status.compareTo("SUCCEEDED") == 0) { + if (status.compareTo("Succeeded") == 0) { finished = true } else { println("$yy status is: $status") diff --git a/kotlin/services/rekognition/src/test/kotlin/RekognitionTest.kt b/kotlin/services/rekognition/src/test/kotlin/RekognitionTest.kt index cab8f00053d..f3af93b966d 100644 --- a/kotlin/services/rekognition/src/test/kotlin/RekognitionTest.kt +++ b/kotlin/services/rekognition/src/test/kotlin/RekognitionTest.kt @@ -1,25 +1,16 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.rekognition.model.NotificationChannel import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson -import com.kotlin.rekognition.addToCollection -import com.kotlin.rekognition.compareTwoFaces import com.kotlin.rekognition.createMyCollection import com.kotlin.rekognition.describeColl -import com.kotlin.rekognition.detectFacesinImage -import com.kotlin.rekognition.detectImageLabels -import com.kotlin.rekognition.detectModLabels -import com.kotlin.rekognition.detectTextLabels -import com.kotlin.rekognition.displayGear import com.kotlin.rekognition.getCelebrityInfo import com.kotlin.rekognition.getFaceResults import com.kotlin.rekognition.getModResults import com.kotlin.rekognition.listAllCollections -import com.kotlin.rekognition.recognizeAllCelebrities import com.kotlin.rekognition.startFaceDetection import com.kotlin.rekognition.startModerationDetection import kotlinx.coroutines.runBlocking @@ -31,11 +22,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class RekognitionTest { + private val logger: Logger = LoggerFactory.getLogger(RekognitionTest::class.java) private var channel: NotificationChannel? = null private var facesImage = "" private var celebritiesImage = "" @@ -77,152 +71,64 @@ class RekognitionTest { modVid = values.modVid.toString() textVid = values.textVid.toString() celVid = values.celVid.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - - // Populate the data members required for all tests. - facesImage = prop.getProperty("facesImage") - celebritiesImage = prop.getProperty("celebritiesImage") - faceImage2 = prop.getProperty("faceImage2") - celId = prop.getProperty("celId") - moutainImage = prop.getProperty("moutainImage") - collectionName = prop.getProperty("collectionName") - ppeImage = prop.getProperty("ppeImage") - bucketName = prop.getProperty("bucketName") - textImage = prop.getProperty("textImage") - modImage = prop.getProperty("modImage") - faceVid = prop.getProperty("faceVid") - topicArn = prop.getProperty("topicArn") - roleArn = prop.getProperty("roleArn") - modVid = prop.getProperty("modVid") - textVid = prop.getProperty("textVid") - celVid = prop.getProperty("celVid") - */ } @Test @Order(1) - fun detectFacesTest() = - runBlocking { - detectFacesinImage(facesImage) - println("Test 1 passed") - } - - @Test - @Order(2) - fun recognizeCelebritiesTest() = - runBlocking { - recognizeAllCelebrities(celebritiesImage) - println("Test 2 passed") - } - - @Test - @Order(3) - fun compareFacesTest() = - runBlocking { - compareTwoFaces(70f, facesImage, faceImage2) - println("Test 3 passed") - } - - @Test - @Order(4) fun celebrityInfoTest() = runBlocking { getCelebrityInfo(celId) - println("Test 4 passed") + logger.info("Test 1 passed") } @Test - @Order(5) - fun detectLabelsTest() = - runBlocking { - detectImageLabels(moutainImage) - println("Test 5 passed") - } - - @Test - @Order(6) + @Order(2) fun createCollectionTest() = runBlocking { createMyCollection(collectionName) - println("Test 6 passed") - } - - @Test - @Order(7) - fun addFacesToCollectionTest() = - runBlocking { - addToCollection(collectionName, facesImage) - println("Test 7 passed") + logger.info("Test 2 passed") } @Test - @Order(8) + @Order(3) fun listFacesCollectionTest() = runBlocking { listAllCollections() - println("Test 8 passed") + logger.info("Test 3 passed") } @Test - @Order(9) + @Order(4) fun listCollectionsTest() = runBlocking { listAllCollections() - println("Test 9 passed") + logger.info("Test 4 passed") } @Test - @Order(10) + @Order(5) fun describeCollectionTest() = runBlocking { describeColl(collectionName) - println("Test 10 passed") + logger.info("Test 5 passed") } @Test - @Order(11) - fun detectPPETest() = - runBlocking { - displayGear(ppeImage) - println("Test 11 passed") - } - - @Test - @Order(12) - fun detectTextTest() = - runBlocking { - detectTextLabels(textImage) - println("Test 12 passed") - } - - @Test - @Order(13) - fun detectModerationLabelsTest() = - runBlocking { - detectModLabels(modImage) - println("Test 13 passed") - } - - @Test - @Order(14) + @Order(6) fun videoDetectFacesTest() = runBlocking { startFaceDetection(channel, bucketName, celVid) getFaceResults() - println("Test 14 passed") + logger.info("Test 6 passed") } @Test - @Order(15) + @Order(7) fun videoDetectInappropriateTest() = runBlocking { startModerationDetection(channel, bucketName, modVid) getModResults() - println("Test 15 passed") + logger.info("Test 7 passed") } private suspend fun getSecretValues(): String { @@ -233,7 +139,6 @@ class RekognitionTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/rekognition/src/test/resources/logback.xml b/kotlin/services/rekognition/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/rekognition/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/route53/build.gradle.kts b/kotlin/services/route53/build.gradle.kts index 36b47242bba..0c37366cdc5 100644 --- a/kotlin/services/route53/build.gradle.kts +++ b/kotlin/services/route53/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { diff --git a/kotlin/services/route53/src/test/kotlin/Route53Test.kt b/kotlin/services/route53/src/test/kotlin/Route53Test.kt index b0afaae98f6..ecdf052ed50 100644 --- a/kotlin/services/route53/src/test/kotlin/Route53Test.kt +++ b/kotlin/services/route53/src/test/kotlin/Route53Test.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -30,11 +29,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class Route53Test { - val dash: String? = String(CharArray(80)).replace("\u0000", "-") + private val logger: Logger = LoggerFactory.getLogger(Route53Test::class.java) private var domainName = "" private var healthCheckId = "" private var hostedZoneId = "" @@ -61,20 +62,6 @@ class Route53Test { firstNameSc = values.firstNameSc.toString() lastNameSc = values.lastNameSc.toString() citySc = values.citySc.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - domainName = prop.getProperty("domainName") - domainSuggestionSc = prop.getProperty("domainSuggestionSc") - domainTypeSc = prop.getProperty("domainTypeSc") - phoneNumerSc = prop.getProperty("phoneNumerSc") - emailSc = prop.getProperty("emailSc") - firstNameSc = prop.getProperty("firstNameSc") - lastNameSc = prop.getProperty("lastNameSc") - citySc = prop.getProperty("citySc") - */ } @Test @@ -83,8 +70,7 @@ class Route53Test { runBlocking { healthCheckId = createCheck(domainName).toString() Assertions.assertFalse(healthCheckId.isEmpty()) - println("The health check id is $healthCheckId") - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -93,8 +79,7 @@ class Route53Test { runBlocking { hostedZoneId = createZone(domainName).toString() Assertions.assertFalse(hostedZoneId.isEmpty()) - println("The hosted zone id is $hostedZoneId") - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -102,7 +87,7 @@ class Route53Test { fun listHealthChecks() = runBlocking { listAllHealthChecks() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -110,7 +95,7 @@ class Route53Test { fun updateHealthCheck() = runBlocking { updateSpecificHealthCheck(healthCheckId) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -118,7 +103,7 @@ class Route53Test { fun listHostedZones() = runBlocking { listZones() - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -126,59 +111,24 @@ class Route53Test { fun deleteHealthCheck() = runBlocking { delHealthCheck(healthCheckId) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @Order(7) fun fullScenarioTest() = runBlocking { - println(dash) - println("1. List current domains.") listDomains() - println(dash) - - println(dash) - println("2. List operations in the past year.") listOperations() - println(dash) - - println(dash) - println("3. View billing for the account in the past year.") listBillingRecords() - println(dash) - - println(dash) - println("4. View prices for domain types.") listAllPrices(domainTypeSc) - println(dash) - - println(dash) - println("5. Get domain suggestions.") listDomainSuggestions(domainSuggestionSc) - println(dash) - - println(dash) - println("6. Check domain availability.") checkDomainAvailability(domainSuggestionSc) - println(dash) - - println(dash) - println("7. Check domain transferability.") checkDomainTransferability(domainSuggestionSc) - println(dash) - - println(dash) - println("8. Request a domain registration.") val opId = requestDomainRegistration(domainSuggestionSc, phoneNumerSc, emailSc, firstNameSc, lastNameSc, citySc) opId?.let { Assertions.assertFalse(it.isEmpty()) } - println(dash) - - println(dash) - println("9. Get operation details.") getOperationalDetail(opId) - println(dash) - println("Test 7 passed") + logger.info("Test 7 passed") } private suspend fun getSecretValues(): String { @@ -189,7 +139,6 @@ class Route53Test { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/route53/src/test/resources/logback.xml b/kotlin/services/route53/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/route53/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/s3/src/test/resources/logback.xml b/kotlin/services/s3/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/s3/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/sagemaker/build.gradle.kts b/kotlin/services/sagemaker/build.gradle.kts index a4801da3ebe..4368450e40e 100644 --- a/kotlin/services/sagemaker/build.gradle.kts +++ b/kotlin/services/sagemaker/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/sagemaker/src/test/kotlin/SageMakerTest.kt b/kotlin/services/sagemaker/src/test/kotlin/SageMakerTest.kt index bfcf358e2f2..379f90f3616 100644 --- a/kotlin/services/sagemaker/src/test/kotlin/SageMakerTest.kt +++ b/kotlin/services/sagemaker/src/test/kotlin/SageMakerTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -22,11 +21,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class SageMakerTest { + private val logger: Logger = LoggerFactory.getLogger(SageMakerTest::class.java) private var image = "" private var modelDataUrl = "" private var executionRoleArn = "" @@ -57,28 +59,6 @@ class SageMakerTest { s3OutputPath = values.s3OutputPath.toString() channelName = values.channelName.toString() trainingImage = values.trainingImage.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - image = prop.getProperty("image") - modelDataUrl = prop.getProperty("modelDataUrl") - executionRoleArn = prop.getProperty("executionRoleArn") - modelName = prop.getProperty("modelName") - s3UriData = prop.getProperty("s3UriData") - s3Uri = prop.getProperty("s3Uri") - roleArn = prop.getProperty("roleArn") - trainingJobName = prop.getProperty("trainingJobName") - s3OutputPath = prop.getProperty("s3OutputPath") - channelName = prop.getProperty("channelName") - trainingImage = prop.getProperty("trainingImage") - s3UriTransform = prop.getProperty("s3UriTransform") - s3OutputPathTransform = prop.getProperty("s3OutputPathTransform") - transformJobName = prop.getProperty("transformJobName") - */ } @Test @@ -86,7 +66,7 @@ class SageMakerTest { fun createModelTest() = runBlocking { createSagemakerModel(modelDataUrl, image, modelName, executionRoleArn) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -94,7 +74,7 @@ class SageMakerTest { fun createTrainingJobTest() = runBlocking { trainJob(s3UriData, s3Uri, trainingJobName, roleArn, s3OutputPath, channelName, trainingImage) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -102,7 +82,7 @@ class SageMakerTest { fun describeTrainingJobTest() = runBlocking { describeTrainJob(trainingJobName) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -110,7 +90,7 @@ class SageMakerTest { fun listModelsTest() = runBlocking { listAllModels() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -118,7 +98,7 @@ class SageMakerTest { fun listNotebooksTest() = runBlocking { listBooks() - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -126,7 +106,7 @@ class SageMakerTest { fun listAlgorithmsTest() = runBlocking { listAlgs() - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -134,7 +114,7 @@ class SageMakerTest { fun listTrainingJobsTest() = runBlocking { listJobs() - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -142,7 +122,7 @@ class SageMakerTest { fun deleteModelTest() = runBlocking { deleteSagemakerModel(modelName) - println("Test 8 passed") + logger.info("Test 8 passed") } private suspend fun getSecretValues(): String { @@ -154,7 +134,6 @@ class SageMakerTest { SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/sagemaker/src/test/resources/logback.xml b/kotlin/services/sagemaker/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/sagemaker/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/secrets-manager/build.gradle.kts b/kotlin/services/secrets-manager/build.gradle.kts index 6c04149f011..c2348b2d40c 100644 --- a/kotlin/services/secrets-manager/build.gradle.kts +++ b/kotlin/services/secrets-manager/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { diff --git a/kotlin/services/secrets-manager/src/test/kotlin/SecretsManagerKotlinTest.kt b/kotlin/services/secrets-manager/src/test/kotlin/SecretsManagerKotlinTest.kt index a3744bdec7d..71eb563cfd8 100644 --- a/kotlin/services/secrets-manager/src/test/kotlin/SecretsManagerKotlinTest.kt +++ b/kotlin/services/secrets-manager/src/test/kotlin/SecretsManagerKotlinTest.kt @@ -7,10 +7,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class SecretsManagerKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(SecretsManagerKotlinTest::class.java) private var secretName = "mysecret" @Test @@ -18,6 +21,6 @@ class SecretsManagerKotlinTest { fun getSecretValue() = runBlocking { getValue(secretName) - println("Test 1 passed") + logger.info("Test 1 passed") } } diff --git a/kotlin/services/secrets-manager/src/test/resources/logback.xml b/kotlin/services/secrets-manager/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/secrets-manager/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/ses/build.gradle.kts b/kotlin/services/ses/build.gradle.kts index 330b911026d..33916c51ec8 100644 --- a/kotlin/services/ses/build.gradle.kts +++ b/kotlin/services/ses/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { implementation("javax.activation:activation:1.1.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { diff --git a/kotlin/services/ses/src/test/kotlin/SESTest.kt b/kotlin/services/ses/src/test/kotlin/SESTest.kt index bdd46212395..7ef323aadb7 100644 --- a/kotlin/services/ses/src/test/kotlin/SESTest.kt +++ b/kotlin/services/ses/src/test/kotlin/SESTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -16,21 +15,18 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class SESTest { + private val logger: Logger = LoggerFactory.getLogger(SESTest::class.java) private var sender = "" private var recipient = "" private var subject = "" private var fileLocation = "" - private val bodyText = - """ - Hello, - Please see the attached file for a list of customers to contact. - """.trimIndent() - private val bodyHTML = """ @@ -52,18 +48,6 @@ class SESTest { recipient = values.recipient.toString() subject = values.subject.toString() fileLocation = values.fileLocation.toString() - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - sender = prop.getProperty("sender") - recipient = prop.getProperty("recipient") - subject = prop.getProperty("subject") - fileLocation = prop.getProperty("fileLocation") - */ } @Test @@ -71,7 +55,7 @@ class SESTest { fun sendMessageTest() = runBlocking { send(sender, recipient, subject, bodyHTML) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -79,7 +63,7 @@ class SESTest { fun listIdentitiesTest() = runBlocking { listSESIdentities() - println("Test 3 passed") + logger.info("Test 2 passed") } private suspend fun getSecretValues(): String { @@ -90,7 +74,6 @@ class SESTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/ses/src/test/resources/logback.xml b/kotlin/services/ses/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/ses/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/sns/build.gradle.kts b/kotlin/services/sns/build.gradle.kts index 83456a63f41..87ea3e709f8 100644 --- a/kotlin/services/sns/build.gradle.kts +++ b/kotlin/services/sns/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/sns/src/test/kotlin/SNSTest.kt b/kotlin/services/sns/src/test/kotlin/SNSTest.kt index 20e1097d20b..a1d0a68b00a 100644 --- a/kotlin/services/sns/src/test/kotlin/SNSTest.kt +++ b/kotlin/services/sns/src/test/kotlin/SNSTest.kt @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider + import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -26,11 +26,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class SNSTest { + private val logger: Logger = LoggerFactory.getLogger(SNSTest::class.java) private var topicName = "" private var topicArn = "" // This value is dynamically set private var subArn = "" // This value is dynamically set @@ -57,21 +60,6 @@ class SNSTest { lambdaarn = values.lambdaarn.toString() phone = values.phone.toString() message = values.message.toString() - - /* - // load the properties file. - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - topicName = prop.getProperty("topicName") - attributeName = prop.getProperty("attributeName") - attributeValue = prop.getProperty("attributeValue") - email = prop.getProperty("email") - lambdaarn = prop.getProperty("lambdaarn") - phone = prop.getProperty("phone") - message = prop.getProperty("message") - existingsubarn = prop.getProperty("existingsubarn") - */ } @Test @@ -80,7 +68,7 @@ class SNSTest { runBlocking { topicArn = createSNSTopic(topicName) Assertions.assertTrue(!topicArn.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -88,7 +76,7 @@ class SNSTest { fun listTopicsTest() = runBlocking { listSNSTopics() - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -96,7 +84,7 @@ class SNSTest { fun setTopicAttributesTest() = runBlocking { setTopAttr(attributeName, topicArn, attributeValue) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -104,7 +92,7 @@ class SNSTest { fun subscribeEmailTest() = runBlocking { subEmail(topicArn, email) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -112,7 +100,7 @@ class SNSTest { fun subscribeLambdaTest() = runBlocking { subLambda(topicArn, lambdaarn) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -120,7 +108,7 @@ class SNSTest { fun addTagsTest() = runBlocking { addTopicTags(topicArn) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -128,7 +116,7 @@ class SNSTest { fun listTagsTest() = runBlocking { listTopicTags(topicArn) - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -136,7 +124,7 @@ class SNSTest { fun deleteTagTest() = runBlocking { removeTag(topicArn, "Team") - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -144,7 +132,7 @@ class SNSTest { fun subEmailTest() = runBlocking { subEmail(topicArn, email) - println("Test 10 passed") + logger.info("Test 10 passed") } @Test @@ -152,7 +140,7 @@ class SNSTest { fun pubTopicTest() = runBlocking { pubTopic(topicArn, message) - println("Test 11 passed") + logger.info("Test 11 passed") } @Test @@ -160,7 +148,7 @@ class SNSTest { fun listSubsTest() = runBlocking { listSNSSubscriptions() - println("Test 12 passed") + logger.info("Test 12 passed") } @Test @@ -168,7 +156,7 @@ class SNSTest { fun subscribeTextSMSTest() = runBlocking { subTextSNS(topicArn, phone) - println("Test 14 passed") + logger.info("Test 14 passed") } @Test @@ -176,7 +164,7 @@ class SNSTest { fun deleteTopicTest() = runBlocking { deleteSNSTopic(topicArn) - println("Test 15 passed") + logger.info("Test 15 passed") } private suspend fun getSecretValues(): String { @@ -187,7 +175,6 @@ class SNSTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/sns/src/test/resources/logback.xml b/kotlin/services/sns/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/sns/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/sqs/build.gradle.kts b/kotlin/services/sqs/build.gradle.kts index f850a57f2e8..3c86901ac38 100644 --- a/kotlin/services/sqs/build.gradle.kts +++ b/kotlin/services/sqs/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { implementation(kotlin("reflect")) implementation("com.fasterxml.jackson.core:jackson-core:2.14.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/sqs/src/test/kotlin/SQSTest.kt b/kotlin/services/sqs/src/test/kotlin/SQSTest.kt index 4df34865a49..7a4ec1d2d3d 100644 --- a/kotlin/services/sqs/src/test/kotlin/SQSTest.kt +++ b/kotlin/services/sqs/src/test/kotlin/SQSTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -24,11 +23,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class SQSTest { + private val logger: Logger = LoggerFactory.getLogger(SQSTest::class.java) private var queueName = "" private var message = "" private var queueUrl = "" @@ -45,16 +47,6 @@ class SQSTest { val queueMessage = gson.fromJson(json, QueueMessage::class.java) queueName = queueMessage.queueName.toString() + randomNum message = queueMessage.message.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // load the properties file. - prop.load(input) - queueName = prop.getProperty("QueueName") - message = prop.getProperty("Message") - */ } @Test @@ -63,7 +55,7 @@ class SQSTest { runBlocking { queueUrl = createQueue(queueName) Assertions.assertTrue(!queueUrl.isEmpty()) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -71,7 +63,7 @@ class SQSTest { fun sendMessageTest() = runBlocking { sendMessages(queueUrl, message) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -79,7 +71,7 @@ class SQSTest { fun sendBatchMessagesTest() = runBlocking { sendBatchMessages(queueUrl) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -87,7 +79,7 @@ class SQSTest { fun getMessageTest() = runBlocking { receiveMessages(queueUrl) - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -95,7 +87,7 @@ class SQSTest { fun addQueueTagsTest() = runBlocking { addTags(queueName) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -103,7 +95,7 @@ class SQSTest { fun listQueueTagsTest() = runBlocking { listTags(queueName) - println("Test 6 passed") + logger.info("Test 6 passed") } @Test @@ -111,7 +103,7 @@ class SQSTest { fun removeQueueTagsTest() = runBlocking { removeTag(queueName, "Test") - println("Test 7 passed") + logger.info("Test 7 passed") } @Test @@ -119,7 +111,7 @@ class SQSTest { fun deleteMessagesTest() = runBlocking { deleteMessages(queueUrl) - println("Test 8 passed") + logger.info("Test 8 passed") } @Test @@ -127,7 +119,7 @@ class SQSTest { fun deleteQueueTest() = runBlocking { deleteQueue(queueUrl) - println("Test 9 passed") + logger.info("Test 9 passed") } private suspend fun getSecretValues(): String { @@ -138,7 +130,6 @@ class SQSTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/sqs/src/test/resources/logback.xml b/kotlin/services/sqs/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/sqs/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/stepfunctions/build.gradle.kts b/kotlin/services/stepfunctions/build.gradle.kts index 7d91e6a14b6..43a8b29d8b2 100644 --- a/kotlin/services/stepfunctions/build.gradle.kts +++ b/kotlin/services/stepfunctions/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt b/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt index 5eb7805d425..494e2bf64b1 100644 --- a/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt +++ b/kotlin/services/stepfunctions/src/test/kotlin/StepFunctionsKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -15,11 +14,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class StepFunctionsKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(StepFunctionsKotlinTest::class.java) private var roleNameSC = "" private var activityNameSC = "" private var stateMachineNameSC = "" @@ -36,18 +38,6 @@ class StepFunctionsKotlinTest { activityNameSC = values.activityNameSC.toString() + UUID.randomUUID() stateMachineNameSC = values.stateMachineNameSC.toString() + UUID.randomUUID() jsonFile = values.machineFile.toString() - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") as InputStream - val prop = Properties() - prop.load(input) - jsonFile = prop.getProperty("jsonFile") - jsonFileSM = prop.getProperty("jsonFileSM") - roleARN = prop.getProperty("roleARN") - stateMachineName = prop.getProperty("stateMachineName") - roleNameSC = prop.getProperty("roleNameSC") - activityNameSC = prop.getProperty("activityNameSC") - stateMachineNameSC = prop.getProperty("stateMachineNameSC") - */ } @Test @@ -55,7 +45,7 @@ class StepFunctionsKotlinTest { fun listStateMachines() = runBlocking { listMachines() - println("Test 4 passed") + logger.info("Test 1 passed") } private suspend fun getSecretValues(): String { @@ -66,7 +56,6 @@ class StepFunctionsKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/stepfunctions/src/test/resources/logback.xml b/kotlin/services/stepfunctions/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/stepfunctions/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/sts/build.gradle.kts b/kotlin/services/sts/build.gradle.kts index 30221cf09d9..4cf2466aa67 100644 --- a/kotlin/services/sts/build.gradle.kts +++ b/kotlin/services/sts/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/sts/src/test/kotlin/Test2.kt b/kotlin/services/sts/src/test/kotlin/STSTest.kt similarity index 78% rename from kotlin/services/sts/src/test/kotlin/Test2.kt rename to kotlin/services/sts/src/test/kotlin/STSTest.kt index ed5247b9325..e7ed3c9f411 100644 --- a/kotlin/services/sts/src/test/kotlin/Test2.kt +++ b/kotlin/services/sts/src/test/kotlin/STSTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -18,10 +17,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) -class Test2 { +class STSTest { + private val logger: Logger = LoggerFactory.getLogger(STSTest::class.java) private var roleArn = "" private var accessKeyId = "" private var roleSessionName = "" @@ -36,49 +38,38 @@ class Test2 { roleArn = values.roleArn.toString() accessKeyId = values.accessKeyId.toString() roleSessionName = values.roleSessionName.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - - // Populate the data members required for all tests. - roleArn = prop.getProperty("roleArn") - accessKeyId = prop.getProperty("accessKeyId") - roleSessionName = prop.getProperty("roleSessionName") - */ } @Test - @Order(2) + @Order(1) fun assumeRoleTest() = runBlocking { assumeGivenRole(roleArn, roleSessionName) - println("Test 2 passed") + logger.info("Test 1 passed") } @Test - @Order(3) + @Order(2) fun getSessionTokenTest() = runBlocking { getToken() - println("Test 3 passed") + logger.info("Test 2 passed") } @Test - @Order(4) + @Order(3) fun getCallerIdentityTest() = runBlocking { getCallerId() - println("Test 4 passed") + logger.info("Test 3 passed") } @Test - @Order(5) + @Order(4) fun getAccessKeyInfoTest() = runBlocking { getKeyInfo(accessKeyId) - println("Test 5 passed") + logger.info("Test 4 passed") } private suspend fun getSecretValues(): String { @@ -89,7 +80,6 @@ class Test2 { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/sts/src/test/resources/logback.xml b/kotlin/services/sts/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/sts/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/support/build.gradle.kts b/kotlin/services/support/build.gradle.kts index 8eb59dde0d3..667dd03221c 100644 --- a/kotlin/services/support/build.gradle.kts +++ b/kotlin/services/support/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/support/src/test/kotlin/SupportTest.kt b/kotlin/services/support/src/test/kotlin/SupportTest.kt index e0109fc5a12..cfc49aab35a 100644 --- a/kotlin/services/support/src/test/kotlin/SupportTest.kt +++ b/kotlin/services/support/src/test/kotlin/SupportTest.kt @@ -8,15 +8,19 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class SupportTest { + private val logger: Logger = LoggerFactory.getLogger(SupportTest::class.java) + @Test @Order(1) fun supportHelloScenario() = runBlocking { displaySomeServices() - println("\n AWS Support Hello Test passed") + logger.info("\n AWS Support Hello Test passed") } } diff --git a/kotlin/services/support/src/test/resources/logback.xml b/kotlin/services/support/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/support/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/textract/build.gradle.kts b/kotlin/services/textract/build.gradle.kts index 0013bc44e93..99a750b840c 100644 --- a/kotlin/services/textract/build.gradle.kts +++ b/kotlin/services/textract/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/textract/src/test/kotlin/TextractTest.kt b/kotlin/services/textract/src/test/kotlin/TextractTest.kt index 6aea7cc73f9..4b139322fad 100644 --- a/kotlin/services/textract/src/test/kotlin/TextractTest.kt +++ b/kotlin/services/textract/src/test/kotlin/TextractTest.kt @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -17,10 +16,13 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class TextractTest { + private val logger: Logger = LoggerFactory.getLogger(TextractTest::class.java) private var sourceDoc = "" private var bucketName = "" private var docName = "" @@ -35,19 +37,6 @@ class TextractTest { sourceDoc = values.sourceDoc.toString() bucketName = values.bucketName.toString() docName = values.docName.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - - // load the properties file. - prop.load(input) - - // Populate the data members required for all tests - sourceDoc = prop.getProperty("sourceDoc") - bucketName = prop.getProperty("bucketName") - docName = prop.getProperty("docName") - */ } @Test @@ -55,7 +44,7 @@ class TextractTest { fun analyzeDocumentTest() = runBlocking { analyzeDoc(sourceDoc) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -63,7 +52,7 @@ class TextractTest { fun detectDocumentTextTest() = runBlocking { detectDocText(sourceDoc) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -71,7 +60,7 @@ class TextractTest { fun detectDocumentTextS3Test() = runBlocking { detectDocTextS3(bucketName, docName) - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -79,7 +68,7 @@ class TextractTest { fun startDocumentAnalysisTest() = runBlocking { startDocAnalysisS3(bucketName, docName) - println("Test 4 passed") + logger.info("Test 4 passed") } private suspend fun getSecretValues(): String { @@ -91,7 +80,6 @@ class TextractTest { SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/textract/src/test/resources/logback.xml b/kotlin/services/textract/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/textract/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/translate/build.gradle.kts b/kotlin/services/translate/build.gradle.kts index e5b87fa2464..b6bc19b1548 100644 --- a/kotlin/services/translate/build.gradle.kts +++ b/kotlin/services/translate/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/translate/src/test/kotlin/TranslateKotlinTest.kt b/kotlin/services/translate/src/test/kotlin/TranslateKotlinTest.kt index d2b3afc869e..3db8628444a 100644 --- a/kotlin/services/translate/src/test/kotlin/TranslateKotlinTest.kt +++ b/kotlin/services/translate/src/test/kotlin/TranslateKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -19,11 +18,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class TranslateKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(TranslateKotlinTest::class.java) private var s3Uri = "" private var s3UriOut = "" private var jobName = "" @@ -41,18 +43,6 @@ class TranslateKotlinTest { s3UriOut = values.s3UriOut.toString() jobName = values.jobName.toString() + UUID.randomUUID() dataAccessRoleArn = values.dataAccessRoleArn.toString() - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - - // Populate the data members required for all tests. - s3Uri = prop.getProperty("s3Uri") - s3UriOut = prop.getProperty("s3UriOut") - jobName = prop.getProperty("jobName") - dataAccessRoleArn = prop.getProperty("dataAccessRoleArn") - */ } @Test @@ -60,7 +50,7 @@ class TranslateKotlinTest { fun translateTextTest() = runBlocking { textTranslate() - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -69,7 +59,7 @@ class TranslateKotlinTest { runBlocking { jobId = translateDocuments(s3Uri, s3UriOut, jobName, dataAccessRoleArn).toString() Assertions.assertTrue(!jobId.isEmpty()) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -77,7 +67,7 @@ class TranslateKotlinTest { fun listTextTranslationJobsTest() = runBlocking { getTranslationJobs() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -85,7 +75,7 @@ class TranslateKotlinTest { fun describeTextTranslationJobTest() = runBlocking { describeTranslationJob(jobId) - println("Test 4 passed") + logger.info("Test 4 passed") } private suspend fun getSecretValues(): String { @@ -96,7 +86,6 @@ class TranslateKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/translate/src/test/resources/logback.xml b/kotlin/services/translate/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/translate/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/kotlin/services/xray/build.gradle.kts b/kotlin/services/xray/build.gradle.kts index 04c0a3e12a7..f7682a215ff 100644 --- a/kotlin/services/xray/build.gradle.kts +++ b/kotlin/services/xray/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10") testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") } tasks.withType { kotlinOptions.jvmTarget = "17" diff --git a/kotlin/services/xray/src/test/kotlin/XrayKotlinTest.kt b/kotlin/services/xray/src/test/kotlin/XrayKotlinTest.kt index a30b04a5990..e7ee6f05770 100644 --- a/kotlin/services/xray/src/test/kotlin/XrayKotlinTest.kt +++ b/kotlin/services/xray/src/test/kotlin/XrayKotlinTest.kt @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import aws.sdk.kotlin.runtime.auth.credentials.EnvironmentCredentialsProvider import aws.sdk.kotlin.services.secretsmanager.SecretsManagerClient import aws.sdk.kotlin.services.secretsmanager.model.GetSecretValueRequest import com.google.gson.Gson @@ -20,11 +19,14 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(OrderAnnotation::class) class XrayKotlinTest { + private val logger: Logger = LoggerFactory.getLogger(XrayKotlinTest::class.java) private var groupName = "" private var newGroupName = "" private var ruleName = "" @@ -42,17 +44,6 @@ class XrayKotlinTest { groupName = values.groupName.toString() newGroupName = values.newGroupName.toString() + randomNum ruleName = values.ruleName.toString() + randomNum - - // Uncomment this code block if you prefer using a config.properties file to retrieve AWS values required for these tests. - - /* - val input: InputStream = this.javaClass.getClassLoader().getResourceAsStream("config.properties") - val prop = Properties() - prop.load(input) - groupName = prop.getProperty("groupName") - newGroupName = prop.getProperty("newGroupName") - ruleName = prop.getProperty("ruleName") - */ } @Test @@ -60,7 +51,7 @@ class XrayKotlinTest { fun createGroup() = runBlocking { createNewGroup(newGroupName) - println("Test 1 passed") + logger.info("Test 1 passed") } @Test @@ -68,7 +59,7 @@ class XrayKotlinTest { fun createSamplingRule() = runBlocking { createRule(ruleName) - println("Test 2 passed") + logger.info("Test 2 passed") } @Test @@ -76,7 +67,7 @@ class XrayKotlinTest { fun getGroups() = runBlocking { getAllGroups() - println("Test 3 passed") + logger.info("Test 3 passed") } @Test @@ -84,7 +75,7 @@ class XrayKotlinTest { fun getSamplingRules() = runBlocking { getRules() - println("Test 4 passed") + logger.info("Test 4 passed") } @Test @@ -92,7 +83,7 @@ class XrayKotlinTest { fun deleteSamplingRule() = runBlocking { deleteRule(ruleName) - println("Test 5 passed") + logger.info("Test 5 passed") } @Test @@ -100,7 +91,7 @@ class XrayKotlinTest { fun deleteGroup() = runBlocking { deleteSpecificGroup(newGroupName) - println("Test 6 passed") + logger.info("Test 6 passed") } private suspend fun getSecretValues(): String { @@ -111,7 +102,6 @@ class XrayKotlinTest { } SecretsManagerClient { region = "us-east-1" - credentialsProvider = EnvironmentCredentialsProvider() }.use { secretClient -> val valueResponse = secretClient.getSecretValue(valueRequest) return valueResponse.secretString.toString() diff --git a/kotlin/services/xray/src/test/resources/logback.xml b/kotlin/services/xray/src/test/resources/logback.xml new file mode 100644 index 00000000000..ba2fdf4e176 --- /dev/null +++ b/kotlin/services/xray/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From c3eb1c09a6e9c715b8802cba0cdffaefe94461b6 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 5 Feb 2025 11:43:04 -0500 Subject: [PATCH 019/144] modified the tests --- javav2/example_code/entityresolution/pom.xml | 9 +++ .../entity/scenario/EntityResScenario.java | 16 ++-- .../src/test/java/EntityResTests.java | 80 ++++++++++++++++--- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index f710bd02f78..7332e236ab5 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -82,10 +82,19 @@ software.amazon.awssdk netty-nio-client + + org.slf4j + slf4j-api + 2.0.13 + software.amazon.awssdk cloudformation + + org.apache.logging.log4j + log4j-slf4j2-impl + \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 3166e662a0c..ff426a2fa5d 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -12,8 +12,6 @@ public class EntityResScenario { public static final String DASHES = new String(new char[80]).replace("\0", "-"); - private static final String ROLES_STACK = "EntityResolutionCdkStack"; - public static void main(String[] args) throws InterruptedException { final String usage = """ @@ -29,19 +27,19 @@ public static void main(String[] args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = "MyMatchingWorkflow451"; - String schemaName = "schema451"; + String workflowName = args[0]; + String schemaName = args[1]; // Use the AWS CDK to create these AWS resources. // See the Readme file located at resources/cdk/entityresolution_resources. - String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; - String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; - String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; - String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + String roleARN = args[2]; + String dataS3bucket = args[3]; + String outputBucket = args[4]; + String inputGlueTableArn = args[5]; EntityResActions actions = new EntityResActions(); Scanner scanner = new Scanner(System.in); - System.out.println("Welcome to the AWS Entity Resolution Scenario. "); + System.out.println("Welcome to the AWS Entity Resolution Scenario."); System.out.println(""" AWS Entity Resolution is a fully-managed machine learning service provided by Amazon Web Services (AWS) that helps organizations extract, link, and diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index 113d9a44bbc..76f6844b1ab 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -3,27 +3,37 @@ import com.example.entity.scenario.EntityResActions; +import com.google.gson.Gson; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; - +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import java.util.Random; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @TestInstance(TestInstance.Lifecycle.PER_METHOD) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class EntityResTests { - + private static final Logger logger = LoggerFactory.getLogger(EntityResTests.class); private static String workflowName = ""; private static String schemaName = ""; private static String roleARN = ""; - private static String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + private static String dataS3bucket = ""; private static String outputBucket = ""; private static String inputGlueTableArn = ""; @@ -35,12 +45,17 @@ public class EntityResTests { private static EntityResActions actions = new EntityResActions(); @BeforeAll public static void setUp() { - workflowName = "MyMatchingWorkflow456"; - schemaName = "schema456"; - roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; - dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; - outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; - inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + Random random = new Random(); + int randomValue = random.nextInt(10000) + 1; + workflowName = "MyMatchingWorkflow"+randomValue; + schemaName = "schema"+randomValue; + Gson gson = new Gson(); + String jsonVal = getSecretValues(); + SecretValues values = gson.fromJson(jsonVal, SecretValues.class); + roleARN = values.getRoleARN(); + dataS3bucket = values.getDataS3bucket(); + outputBucket = values.getOutputBucket(); + inputGlueTableArn = values.getInputGlueTableArn(); String json = """ [ @@ -77,6 +92,7 @@ public void testCreateMapping() { mappingARN = response.schemaArn(); assertNotNull(mappingARN); }); + logger.info("Test 1 passed"); } @Test @@ -87,6 +103,7 @@ public void testCreateMappingWorkflow() { workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); assertNotNull(workflowArn); }); + logger.info("Test 2 passed"); } @Test @@ -97,6 +114,7 @@ public void testStartWorkflow() { jobId = actions.startMatchingJobAsync(workflowName).join(); assertNotNull(workflowArn); }); + logger.info("Test 3 passed"); } @Test @@ -106,6 +124,7 @@ public void testGetJobDetails() { assertDoesNotThrow(() -> { actions.getMatchingJobAsync(jobId, workflowName).join(); }); + logger.info("Test 4 passed"); } @Test @@ -115,6 +134,7 @@ public void testtSchemaMappingDetails() { assertDoesNotThrow(() -> { actions.getSchemaMappingAsync(schemaName).join(); }); + logger.info("Test 5 passed"); } @Test @@ -124,6 +144,7 @@ public void testListSchemaMappings() { assertDoesNotThrow(() -> { actions.ListSchemaMappings(); }); + logger.info("Test 6 passed"); } @Test @@ -133,6 +154,7 @@ public void testLTagResources() { assertDoesNotThrow(() -> { actions.tagEntityResource(mappingARN).join(); }); + logger.info("Test 7 passed"); } @Test @@ -144,7 +166,47 @@ public void testLDeleteMapping() { Thread.sleep(1800000); actions.deleteMatchingWorkflowAsync(workflowName).join(); }); + logger.info("Test 8 passed"); + } + + private static String getSecretValues() { + SecretsManagerClient secretClient = SecretsManagerClient.builder() + .region(Region.US_EAST_1) + .build(); + String secretName = "test/entity"; + + GetSecretValueRequest valueRequest = GetSecretValueRequest.builder() + .secretId(secretName) + .build(); + + GetSecretValueResponse valueResponse = secretClient.getSecretValue(valueRequest); + return valueResponse.secretString(); } + @Nested + @DisplayName("A class used to get test values from test/cognito (an AWS Secrets Manager secret)") + class SecretValues { + private String roleARN; + private String dataS3bucket; + + private String outputBucket; + + private String inputGlueTableArn; + public String getRoleARN() { + return roleARN; + } + + public String getDataS3bucket() { + return dataS3bucket; + } + + public String getOutputBucket() { + return outputBucket; + } + + public String getInputGlueTableArn() { + return inputGlueTableArn; + } + } } From be50acb4a53f5193154cbb887f9056d62394de31 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 5 Feb 2025 13:38:31 -0500 Subject: [PATCH 020/144] modified the tests --- javav2/example_code/entityresolution/pom.xml | 22 ++- .../entity/scenario/EntityResActions.java | 30 ++-- .../entity/scenario/EntityResScenario.java | 135 +++++++++--------- .../src/main/resources/log4j2.xml | 18 +++ javav2/example_code/iotsitewise/pom.xml | 8 +- 5 files changed, 124 insertions(+), 89 deletions(-) create mode 100644 javav2/example_code/entityresolution/src/main/resources/log4j2.xml diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index 7332e236ab5..0e9154ddd6f 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -35,6 +35,13 @@ pom import + + org.apache.logging.log4j + log4j-bom + 2.23.1 + pom + import + @@ -82,19 +89,26 @@ software.amazon.awssdk netty-nio-client + + software.amazon.awssdk + cloudformation + + + org.apache.logging.log4j + log4j-core + org.slf4j slf4j-api 2.0.13 - software.amazon.awssdk - cloudformation + org.apache.logging.log4j + log4j-slf4j2-impl org.apache.logging.log4j - log4j-slf4j2-impl + log4j-1.2-api - \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 4a49c8c09af..b2273c2226a 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -9,9 +9,9 @@ import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.entityresolution.EntityResolutionClient; import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowRequest; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowResponse; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; @@ -35,7 +35,6 @@ import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; - import java.time.Duration; import java.util.HashMap; import java.util.List; @@ -43,8 +42,7 @@ import java.util.concurrent.CompletableFuture; public class EntityResActions { - - private EntityResolutionClient resolutionClient; + private static final Logger logger = LoggerFactory.getLogger(EntityResActions.class); private static EntityResolutionAsyncClient entityResolutionAsyncClient; @@ -128,7 +126,7 @@ public void ListSchemaMappings() { // Iterate through the pages of results CompletableFuture future = paginator.subscribe(response -> { response.schemaList().forEach(schemaMapping -> - System.out.println("Schema Mapping Name: " +schemaMapping.schemaName()) + logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) ); }); @@ -182,7 +180,7 @@ public CompletableFuture createSchemaMappingAsync(S return getResolutionAsyncClient().createSchemaMapping(request) .whenComplete((response, exception) -> { if (response != null) { - System.out.println("Schema Mapping Created Successfully!"); + logger.info("Schema Mapping Created Successfully!"); } else { throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); } @@ -207,7 +205,7 @@ public CompletableFuture getSchemaMappingAsync(String .whenComplete((response, exception) -> { if (response != null) { response.mappedInputFields().forEach(attribute -> - System.out.println("Attribute Name: " + attribute.fieldName() + + logger.info("Attribute Name: " + attribute.fieldName() + ", Attribute Type: " + attribute.type().toString())); } else { throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); @@ -232,8 +230,8 @@ public CompletableFuture getMatchingJobAsync(String jobId, String workflow return getResolutionAsyncClient().getMatchingJob(request) .thenAccept(response -> { - System.out.println("Job status: " + response.status()); - System.out.println("Job details: " + response.toString()); + logger.info("Job status: " + response.status()); + logger.info("Job details: " + response.toString()); }) .exceptionally(ex -> { throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); @@ -258,7 +256,7 @@ public CompletableFuture startMatchingJobAsync(String workflowName) { if (response != null) { // Get the job ID from the response String jobId = response.jobId(); - System.out.println("Job ID: " + jobId); + logger.info("Job ID: " + jobId); } else { // Handle the exception if the response is null throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); @@ -284,11 +282,11 @@ public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, return getResolutionAsyncClient().getMatchingJob(request) .thenApply(response -> { - System.out.println("\nJob status: " + response.status()); + logger.info("\nJob status: " + response.status()); return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); }) .exceptionally(exception -> { - System.out.println("Error checking workflow status: " + exception.getMessage()); + logger.info("Error checking workflow status: " + exception.getMessage()); return false; }); } @@ -336,7 +334,7 @@ public CompletableFuture createMatchingWorkflowAsync(String roleARN, Str return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) .whenComplete((response, exception) -> { if (response != null) { - System.out.println("Workflow created successfully."); + logger.info("Workflow created successfully."); } else { throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); } @@ -362,7 +360,7 @@ public CompletableFuture tagEntityResource(String schemaMappingARN) { .build(); return getResolutionAsyncClient().tagResource(request) - .thenAccept(response -> System.out.println("Successfully tagged the resource.")) + .thenAccept(response -> logger.info("Successfully tagged the resource.")) .exceptionally(exception -> { throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); }); @@ -414,6 +412,4 @@ public boolean doesObjectExist(String bucketName) { return false; } } - - } diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index ff426a2fa5d..a5598894800 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -6,11 +6,13 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; -import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Scanner; import java.util.concurrent.CompletionException; public class EntityResScenario { + private static final Logger logger = LoggerFactory.getLogger(EntityResScenario.class); public static final String DASHES = new String(new char[80]).replace("\0", "-"); public static void main(String[] args) throws InterruptedException { @@ -27,20 +29,26 @@ public static void main(String[] args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = args[0]; - String schemaName = args[1]; + + // if (args.length != 6) { + // logger.info(usage); + // return; + // } + + String workflowName = "workflow100" ; //args[0]; + String schemaName = "schemaName100" ;//args[1]; // Use the AWS CDK to create these AWS resources. // See the Readme file located at resources/cdk/entityresolution_resources. - String roleARN = args[2]; - String dataS3bucket = args[3]; - String outputBucket = args[4]; - String inputGlueTableArn = args[5]; + String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm" ; //args[2]; + String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d" ; //args[3]; + String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; //args[4]; + String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution" ; //args[5]; EntityResActions actions = new EntityResActions(); Scanner scanner = new Scanner(System.in); - System.out.println("Welcome to the AWS Entity Resolution Scenario."); - System.out.println(""" + logger.info("Welcome to the AWS Entity Resolution Scenario."); + logger.info(""" AWS Entity Resolution is a fully-managed machine learning service provided by Amazon Web Services (AWS) that helps organizations extract, link, and organize information from multiple data sources. It leverages natural @@ -66,10 +74,9 @@ Amazon Web Services (AWS) that helps organizations extract, link, and """); waitForInputToContinue(scanner); - System.out.println(DASHES); - - System.out.println(DASHES); + logger.info(DASHES); + logger.info(DASHES); /* This JSON is a valid input for the AWS Entity Resolution service. The JSON represents an array of three objects, each containing an "id", "name", and "email" @@ -95,20 +102,20 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } ] """; - System.out.println("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); - System.out.println(json); + logger.info("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); + logger.info(json); waitForInputToContinue(scanner); if (!actions.doesObjectExist(dataS3bucket)) { actions.uploadLocalFileAsync(dataS3bucket, json); } else { - System.out.println("The JSON exists in " + dataS3bucket); + logger.info("The JSON exists in " + dataS3bucket); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("1. Create Schema Mapping"); - System.out.println(""" + logger.info(DASHES); + logger.info("1. Create Schema Mapping"); + logger.info(""" Entity Resolution Schema Mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, @@ -125,15 +132,15 @@ Amazon Web Services (AWS) that helps organizations extract, link, and mappingARN = response.schemaArn(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("2. Create an AWS Entity Resolution Workflow. "); - System.out.println(""" + logger.info(DASHES); + logger.info("2. Create an AWS Entity Resolution Workflow. "); + logger.info(""" An Entity Resolution matching workflow identifies and links records across datasets that represent the same real-world entity, such as customers or products. Using techniques like schema mapping, @@ -145,59 +152,59 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); try { String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); - System.out.println("The workflow ARN is: " + workflowArn); + logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); - System.out.println(DASHES); - System.out.println("3. Start the matching job of the " + workflowName + " workflow."); + logger.info(DASHES); + logger.info("3. Start the matching job of the " + workflowName + " workflow."); waitForInputToContinue(scanner); String jobId = null; try { jobId = actions.startMatchingJobAsync(workflowName).join(); - System.out.println("The matching job was successfully started."); + logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("4. Get details for job "+jobId); + logger.info(DASHES); + logger.info("4. Get details for job "+jobId); waitForInputToContinue(scanner); try { actions.getMatchingJobAsync(jobId, workflowName).join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); } - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("5. Get Schema Mapping."); + logger.info(DASHES); + logger.info("5. Get Schema Mapping."); waitForInputToContinue(scanner); try { actions.getSchemaMappingAsync(schemaName).join(); - System.out.println("Schema mapping retrieval completed."); + logger.info("Schema mapping retrieval completed."); } catch (CompletionException ce) { - System.err.println("Error retrieving schema mapping: " + ce.getCause().getMessage()); + logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("6. List Schema Mappings."); + logger.info(DASHES); + logger.info("6. List Schema Mappings."); actions.ListSchemaMappings(); waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("7. Tag the "+schemaName +"resource."); - System.out.println(""" + logger.info(DASHES); + logger.info("7. Tag the "+schemaName +"resource."); + logger.info(""" Tags can help you organize and categorize your Entity Resolution resources. You can also use them to scope user permissions by granting a user permission to access or change only resources with certain tag values. @@ -206,51 +213,51 @@ Amazon Web Services (AWS) that helps organizations extract, link, and """); actions.tagEntityResource(mappingARN).join(); waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("8. Delete the AWS Entity Resolution Workflow."); - System.out.println(""" + logger.info(DASHES); + logger.info("8. Delete the AWS Entity Resolution Workflow."); + logger.info(""" You cannot delete a workflow that is in a running state. Would you like to wait for the workflow to complete. This can take up to 30 mins (y/n). """); String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { - System.out.println("You selected to delete Entity Resolution Workflow."); + logger.info("You selected to delete Entity Resolution Workflow."); waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); try { actions.deleteMatchingWorkflowAsync(workflowName).join(); - System.out.println("Workflow deleted successfully!"); + logger.info("Workflow deleted successfully!"); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("This concludes the AWS Entity Resolution scenario."); - System.out.println(DASHES); + logger.info(DASHES); + logger.info("This concludes the AWS Entity Resolution scenario."); + logger.info(DASHES); } private static void waitForInputToContinue(Scanner scanner) { while (true) { - System.out.println(""); - System.out.println("Enter 'c' followed by to continue:"); + logger.info(""); + logger.info("Enter 'c' followed by to continue:"); String input = scanner.nextLine(); if (input.trim().equalsIgnoreCase("c")) { - System.out.println("Continuing with the program..."); - System.out.println(""); + logger.info("Continuing with the program..."); + logger.info(""); break; } else { // Handle invalid input. - System.out.println("Invalid input. Please try again."); + logger.info("Invalid input. Please try again."); } } } @@ -272,8 +279,8 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota // Check workflow status every 60 seconds if (secondsElapsed % 60 == 0 || remainingTime <= 0) { if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { - System.out.println(); // Move to the next line after countdown - System.out.println("Countdown complete: Workflow is in SUCCEEDED state!"); + logger.info(""); // Move to the next line after countdown + logger.info("Countdown complete: Workflow is in SUCCEEDED state!"); break; } } diff --git a/javav2/example_code/entityresolution/src/main/resources/log4j2.xml b/javav2/example_code/entityresolution/src/main/resources/log4j2.xml new file mode 100644 index 00000000000..225afe2b3a8 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/javav2/example_code/iotsitewise/pom.xml b/javav2/example_code/iotsitewise/pom.xml index ce7da2101c6..e98a1f632d7 100644 --- a/javav2/example_code/iotsitewise/pom.xml +++ b/javav2/example_code/iotsitewise/pom.xml @@ -82,6 +82,10 @@ software.amazon.awssdk ssooidc + + software.amazon.awssdk + cloudformation + org.apache.logging.log4j log4j-core @@ -91,10 +95,6 @@ slf4j-api 2.0.13 - - software.amazon.awssdk - cloudformation - org.apache.logging.log4j log4j-slf4j2-impl From 7a5c7066f610be1ddbd3fc6eedbb968b3b6bf6e7 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 5 Feb 2025 14:54:44 -0500 Subject: [PATCH 021/144] modified the tests --- .../entityresolution/src/test/java/EntityResTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index 76f6844b1ab..5e915b8f0ad 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -162,7 +162,7 @@ public void testLTagResources() { @Order(8) public void testLDeleteMapping() { assertDoesNotThrow(() -> { - System.out.println("Wait 30 mins for the workflow to complete"); + logger.info("Wait 30 mins for the workflow to complete"); Thread.sleep(1800000); actions.deleteMatchingWorkflowAsync(workflowName).join(); }); From ad6c9b7cb00204eccdd8832bcdc3a08c8c66da99 Mon Sep 17 00:00:00 2001 From: ford prior Date: Wed, 5 Feb 2025 17:18:12 -0500 Subject: [PATCH 022/144] 2025.05.1 (#7230) --- .github/workflows/validate-doc-metadata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-doc-metadata.yml b/.github/workflows/validate-doc-metadata.yml index 1948f05ccce..c048e1aa3e2 100644 --- a/.github/workflows/validate-doc-metadata.yml +++ b/.github/workflows/validate-doc-metadata.yml @@ -16,7 +16,7 @@ jobs: - name: checkout repo content uses: actions/checkout@v4 - name: validate metadata - uses: awsdocs/aws-doc-sdk-examples-tools@2025.02.0 + uses: awsdocs/aws-doc-sdk-examples-tools@2025.05.1 with: doc_gen_only: "False" strict_titles: "True" From 36fdd6ae241a6aa1dce64d61e7402bf816395dc1 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 6 Feb 2025 09:34:20 -0500 Subject: [PATCH 023/144] added new SOS Yaml file --- .doc_gen/metadata/entityresolution_metadata.yaml | 12 ++++++++++++ .../example/entity/scenario/EntityResScenario.java | 2 -- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .doc_gen/metadata/entityresolution_metadata.yaml diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml new file mode 100644 index 00000000000..971641b7df2 --- /dev/null +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -0,0 +1,12 @@ +entityresolution_CreateSchemaMapping: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_create_schema.main + services: + entityresolution: {CreateSchemaMapping} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index a5598894800..cacb3763921 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -241,8 +241,6 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info("This concludes the AWS Entity Resolution scenario."); logger.info(DASHES); - - } private static void waitForInputToContinue(Scanner scanner) { From 71dbecda98adbc1ebef712f485cf965a643ca836 Mon Sep 17 00:00:00 2001 From: ford prior Date: Thu, 6 Feb 2025 11:36:21 -0500 Subject: [PATCH 024/144] Tools - Validator: Add allowlist entry to validation.yaml (#7234) --- .doc_gen/validation.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.doc_gen/validation.yaml b/.doc_gen/validation.yaml index 3aadb80233f..ea3da0450aa 100644 --- a/.doc_gen/validation.yaml +++ b/.doc_gen/validation.yaml @@ -211,6 +211,7 @@ allow_list: - "src/main/java/com/example/acm/DeleteCert" - "src/main/java/com/example/acm/ImportCert" - "EnablePropagateAdditionalUserContextData" + - "StopQueryWorkloadInsightsTopContributors" sample_files: - "README.md" - "chat_sfn_state_machine.json" From 8d47a81960aedd9e1b4d0257721d8e55199bbaa5 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 6 Feb 2025 11:47:05 -0500 Subject: [PATCH 025/144] updated a comment --- .../main/java/com/example/entity/scenario/EntityResActions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index b2273c2226a..1a605fe11f1 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -162,7 +162,7 @@ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) /** * Creates a schema mapping asynchronously. * - * @param schemaName the name of the schema to create the mapping for + * @param schemaName the name of the schema to create * @return a {@link CompletableFuture} that represents the asynchronous creation of the schema mapping */ public CompletableFuture createSchemaMappingAsync(String schemaName) { From 067fcd69bad9ea6a507f89fcad2bc7dfe6db118d Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 6 Feb 2025 14:22:25 -0500 Subject: [PATCH 026/144] updated the YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 971641b7df2..400cd855972 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -1,3 +1,15 @@ +entityresolution_GetSchemaMapping: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_get_schema_mapping.main + services: + entityresolution: {GetSchemaMapping} entityresolution_CreateSchemaMapping: languages: Java: From 8792ff85388c1551a9e79d9a0a0ed5f0460b9d38 Mon Sep 17 00:00:00 2001 From: ford prior Date: Thu, 6 Feb 2025 17:31:36 -0500 Subject: [PATCH 027/144] Tools: Weathertop - Add Account Nuker (#7203) --- .../stacks/nuke/typescript/.prettierignore | 1 + .tools/test/stacks/nuke/typescript/Dockerfile | 12 ++ .tools/test/stacks/nuke/typescript/README.md | 52 ++++++ .../stacks/nuke/typescript/account_nuker.ts | 65 ++++++++ .tools/test/stacks/nuke/typescript/cdk.json | 33 ++++ .../nuke/typescript/create_account_alias.py | 118 +++++++++++++ .../nuke/typescript/nuke-architecture.jpg | Bin 0 -> 778619 bytes .../nuke/typescript/nuke_generic_config.yaml | 157 ++++++++++++++++++ .../test/stacks/nuke/typescript/package.json | 27 +++ .tools/test/stacks/nuke/typescript/run.sh | 15 ++ .../test/stacks/nuke/typescript/tsconfig.json | 23 +++ 11 files changed, 503 insertions(+) create mode 100644 .tools/test/stacks/nuke/typescript/.prettierignore create mode 100644 .tools/test/stacks/nuke/typescript/Dockerfile create mode 100644 .tools/test/stacks/nuke/typescript/README.md create mode 100644 .tools/test/stacks/nuke/typescript/account_nuker.ts create mode 100644 .tools/test/stacks/nuke/typescript/cdk.json create mode 100644 .tools/test/stacks/nuke/typescript/create_account_alias.py create mode 100644 .tools/test/stacks/nuke/typescript/nuke-architecture.jpg create mode 100644 .tools/test/stacks/nuke/typescript/nuke_generic_config.yaml create mode 100644 .tools/test/stacks/nuke/typescript/package.json create mode 100755 .tools/test/stacks/nuke/typescript/run.sh create mode 100644 .tools/test/stacks/nuke/typescript/tsconfig.json diff --git a/.tools/test/stacks/nuke/typescript/.prettierignore b/.tools/test/stacks/nuke/typescript/.prettierignore new file mode 100644 index 00000000000..41857269f92 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/.prettierignore @@ -0,0 +1 @@ +cdk.out/ diff --git a/.tools/test/stacks/nuke/typescript/Dockerfile b/.tools/test/stacks/nuke/typescript/Dockerfile new file mode 100644 index 00000000000..d451651bf7c --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/ekristen/aws-nuke:v3.42.0 +ENV AWS_SDK_LOAD_CONFIG=1 \ + AWS_DEBUG=true +USER root +RUN apk add --no-cache \ + python3 \ + py3-pip \ + aws-cli +COPY nuke_generic_config.yaml /nuke_generic_config.yaml +COPY --chmod=755 run.sh /run.sh +USER aws-nuke +ENTRYPOINT ["/run.sh"] diff --git a/.tools/test/stacks/nuke/typescript/README.md b/.tools/test/stacks/nuke/typescript/README.md new file mode 100644 index 00000000000..2ebe1c3fc0e --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/README.md @@ -0,0 +1,52 @@ +# aws-nuke for Weathertop + +[aws-nuke](https://github.com/ekristen/aws-nuke) is an open-source tool that deletes non-default resources in a provided AWS account. It's implemented here in this directory using Cloud Development Kit (CDK) code that deploys the [official aws-nuke image](https://github.com/ekristen/aws-nuke/pkgs/container/aws-nuke) to an AWS Lambda function. + +## ⚠ Important + +This is a very destructive tool! It should not be deployed without fully understanding the impact it will have on your AWS accounts. +Please use caution and configure this tool to delete unused resources only in your lower test/sandbox environment accounts. + +## Overview + +This CDK stack is defined in [account_nuker.ts](account_nuker.ts). It includes: + +- A Docker-based Lambda function with ARM64 architecture and 1GB memory +- An IAM role with administrative permissions for the Lambda's nuking function +- An EventBridge rule that triggers the function every Sunday at midnight + +More specifically, this Lambda function is built from a [Dockerfile](Dockerfile) and runs with a 15-minute timeout. It contains a [nuke_generic_config.yml](nuke_generic_config.yaml) config and executes a [run.sh](run.sh) when invoked every Sunday at midnight UTC. + +![infrastructure-overview](nuke-overview.png) + +## Prerequisites + +1. **Non-Prod AWS Account Alias**: A non-prod account alias must exist in target account. Set the alias by running `python create_account_alias.py weathertop-test` or following [these instructions](https://docs.aws.amazon.com/IAM/latest/UserGuide/account-alias-create.html). + +## Setup and Installation + +For multi-account deployments, please use the [deploy.py](../../../DEPLOYMENT.md#option-1-using-deploypy) script. + +For single-account deployment, you can just run: + +```sh +cdk bootstrap && cdk deploy +``` + +Note a successful stack creation, e.g.: + +```bash +NukeStack: success: Published 956fbd116734e79edb987e767fe7f45d0b97e2123456789109103f80ba4c1:123456789101-us-east-1 +Stack undefined +NukeStack: deploying... [1/1] +NukeStack: creating CloudFormation changeset... + + ✅ NukeStack + +✨ Deployment time: 27.93s + +Stack ARN: +arn:aws:cloudformation:us-east-1:123456789101:stack/NukeStack/9835cc20-d358-11ef-bccf-123407dc82dd + +✨ Total time: 33.24s +``` diff --git a/.tools/test/stacks/nuke/typescript/account_nuker.ts b/.tools/test/stacks/nuke/typescript/account_nuker.ts new file mode 100644 index 00000000000..2698d657ad9 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/account_nuker.ts @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as cdk from "aws-cdk-lib"; +import * as events from "aws-cdk-lib/aws-events"; +import * as targets from "aws-cdk-lib/aws-events-targets"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as path from "path"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import { Duration, Stack, StackProps } from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda"; + +export interface NukeStackProps extends cdk.StackProps { + awsNukeDryRunFlag?: string; + awsNukeVersion?: string; + owner?: string; +} + +class NukeStack extends cdk.Stack { + private readonly nukeLambdaRole: iam.Role; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + // Lambda Function role + this.nukeLambdaRole = new iam.Role(this, "NukeLambdaRole", { + assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"), + ], + }); + + // Create the Lambda function + const lambdaFunction = new DockerImageFunction( + this, + "docker-lambda-function", + { + functionName: "docker-lambda-fn", + code: DockerImageCode.fromImageAsset(path.join(__dirname)), + memorySize: 1024, + timeout: Duration.minutes(15), + architecture: lambda.Architecture.ARM_64, + description: "This is dockerized AWS Lambda function", + role: this.nukeLambdaRole, + }, + ); + + // Create EventBridge rule to trigger the Lambda function weekly + const rule = new events.Rule(this, "WeeklyTriggerRule", { + schedule: events.Schedule.expression("cron(0 0 ? * SUN *)"), // Runs at 00:00 every Sunday + }); + + // Add the Lambda function as a target for the EventBridge rule + rule.addTarget(new targets.LambdaFunction(lambdaFunction)); + } +} + +const app = new cdk.App(); +new NukeStack(app, "NukeStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + terminationProtection: true, +}); diff --git a/.tools/test/stacks/nuke/typescript/cdk.json b/.tools/test/stacks/nuke/typescript/cdk.json new file mode 100644 index 00000000000..b75b3c38598 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/cdk.json @@ -0,0 +1,33 @@ +{ + "app": "npx ts-node --prefer-ts-exts account_nuker.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "cdk-migrate": true + } +} diff --git a/.tools/test/stacks/nuke/typescript/create_account_alias.py b/.tools/test/stacks/nuke/typescript/create_account_alias.py new file mode 100644 index 00000000000..c2e4601a843 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/create_account_alias.py @@ -0,0 +1,118 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This module is used to create an AWS account alias, which is required by the deploy.py script. + +It provides a function to create an account alias using the AWS CLI, as this specific +operation is not supported by the AWS CDK. +""" + +import logging +import re +import subprocess + +logger = logging.getLogger(__name__) + + +def _is_valid_alias(alias_name: str) -> bool: + """ + Check if the provided alias name is valid according to AWS rules. + + AWS account alias must be unique and must be between 3 and 63 characters long. + Valid characters are a-z, 0-9 and '-'. + + Args: + alias_name (str): The alias name to validate. + + Returns: + bool: True if the alias is valid, False otherwise. + """ + pattern = r"^[a-z0-9](([a-z0-9]|-){0,61}[a-z0-9])?$" + return bool(re.match(pattern, alias_name)) and 3 <= len(alias_name) <= 63 + + +def _log_aws_cli_version() -> None: + """ + Log the version of the AWS CLI installed on the system. + """ + try: + result = subprocess.run(["aws", "--version"], capture_output=True, text=True) + logger.info(f"AWS CLI version: {result.stderr.strip()}") + except Exception as e: + logger.warning(f"Unable to determine AWS CLI version: {str(e)}") + + +def create_account_alias(alias_name: str) -> None: + """ + Create a new account alias with the given name. + + This function exists because the CDK does not support the specific + CreateAccountAliases API call. It attempts to create an account alias + using the AWS CLI and logs the result. + + If the account alias is created successfully, it logs a success message. + If the account alias already exists, it logs a message indicating that. + If there is any other error, it logs the error message. + + Args: + alias_name (str): The desired name for the account alias. + """ + # Log AWS CLI version when the function is called + _log_aws_cli_version() + + if not _is_valid_alias(alias_name): + logger.error( + f"Invalid alias name '{alias_name}'. It must be between 3 and 63 characters long and contain only lowercase letters, numbers, and hyphens." + ) + return + + command = ["aws", "iam", "create-account-alias", "--account-alias", alias_name] + + try: + subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + logger.info(f"Account alias '{alias_name}' created successfully.") + except subprocess.CalledProcessError as e: + if "EntityAlreadyExists" in e.stderr: + logger.info(f"Account alias '{alias_name}' already exists.") + elif "AccessDenied" in e.stderr: + logger.error( + f"Access denied when creating account alias '{alias_name}'. Check your AWS credentials and permissions." + ) + elif "ValidationError" in e.stderr: + logger.error( + f"Validation error when creating account alias '{alias_name}'. The alias might not meet AWS requirements." + ) + else: + logger.error(f"Error creating account alias '{alias_name}': {e.stderr}") + except Exception as e: + logger.error( + f"Unexpected error occurred while creating account alias '{alias_name}': {str(e)}" + ) + + +def main(): + import argparse + + # Set up logging + logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" + ) + + # Create argument parser + parser = argparse.ArgumentParser(description="Create an AWS account alias") + parser.add_argument("alias", help="The alias name for the AWS account") + + # Parse arguments + args = parser.parse_args() + + # Call the function with the provided alias + create_account_alias(args.alias) + +if __name__ == "__main__": + main() diff --git a/.tools/test/stacks/nuke/typescript/nuke-architecture.jpg b/.tools/test/stacks/nuke/typescript/nuke-architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c5a69c71509a923a73134b5d493d42884e5102a6 GIT binary patch literal 778619 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 z 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%0DHQpx`&%R3(Ia4Cr}zY^0}{9x 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 literal 0 HcmV?d00001 diff --git a/.tools/test/stacks/nuke/typescript/nuke_generic_config.yaml b/.tools/test/stacks/nuke/typescript/nuke_generic_config.yaml new file mode 100644 index 00000000000..261b2c35950 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/nuke_generic_config.yaml @@ -0,0 +1,157 @@ +regions: + - us-east-1 + +blocklist: + # Must have 1+ blocklist entry (see https://aws-nuke.ekristen.dev/warning/) + - 000000000000 + +resource-types: + excludes: + - ACMCertificate + - AWSBackupPlan + - AWSBackupRecoveryPoint + - AWSBackupSelection + - AWSBackupVault + - AWSBackupVaultAccessPolicy + - CloudTrailTrail + - CloudWatchEventsTarget + - CodeCommitRepository + - CodeStarProject + - ConfigServiceConfigRule + - ECRRepository + - EC2Address + - EC2ClientVpnEndpoint + - EC2ClientVpnEndpointAttachment + - EC2CustomerGateway + - EC2DHCPOption + - EC2DefaultSecurityGroupRule + - EC2EgressOnlyInternetGateway + - EC2InternetGateway + - EC2InternetGatewayAttachment + - EC2KeyPair + - EC2NetworkACL + - EC2NetworkInterface + - EC2RouteTable + - EC2SecurityGroup + - EC2Subnet + - EC2VPC + - EC2VPCEndpoint + - IAMGroup + - IAMGroupPolicy + - IAMGroupPolicyAttachment + - IAMInstanceProfile + - IAMInstanceProfileRole + - IAMLoginProfile + - IAMOpenIDConnectProvider + - IAMPolicy + - IAMRole + - IAMRolePolicy + - IAMRolePolicyAttachment + - IAMSAMLProvider + - IAMServerCertificate + - IAMServiceSpecificCredential + - IAMSigningCertificate + - IAMUser + - IAMUserAccessKey + - IAMUserGroupAttachment + - IAMUserPolicy + - IAMUserPolicyAttachment + - IAMUserSSHPublicKey + - IAMVirtualMFADevice + - KMSAlias + - KMSKey + - Route53HostedZone + - Route53ResourceRecordSet + - S3Bucket + - S3Object + - SecretsManagerSecret + - SQSQueue + - SSMParameter + +accounts: + AWSACCOUNTID: + filters: + EC2VPC: + - property: IsDefault + value: "true" + EC2DHCPOption: + - property: DefaultVPC + value: "true" + EC2InternetGateway: + - property: DefaultVPC + value: "true" + EC2InternetGatewayAttachment: + - property: DefaultVPC + value: "true" + EC2Subnet: + - property: DefaultVPC + value: "true" + EC2RouteTable: + - property: DefaultVPC + value: "true" + EC2DefaultSecurityGroupRule: + - property: SecurityGroupId + type: glob + value: "*" + LambdaEventSourceMapping: + - property: "EventSourceArn" + type: "glob" + value: "^(PluginStack|NukeStack)*$" + - property: "FunctionArn" + type: "glob" + value: "^(PluginStack|NukeStack)*$" + LambdaPermission: + - property: "name" + type: "glob" + value: "^(PluginStack|NukeStack)*$" + GuardDutyDetector: + - property: DetectorID + type: glob + value: "*" + CloudWatchEventsRule: + - type: regex + value: "^Rule: (AwsSecurity.*)$" + CloudWatchEventsTarget: + - type: regex + value: "^Rule: (AwsSecurity.*)$" + CloudWatchLogsLogGroup: + - type: regex + value: "^.*$" + ConfigServiceDeliveryChannel: + - "default" + ConfigServiceConfigRule: + - type: regex + value: "^(managed-ec2-patch-compliance|ec2-managed-by-systems-manager-REMEDIATE)$" + S3Bucket: + - property: Name + type: regex + value: "^(cdktoolkit-stagingbucket-.*|aws-nuke.*)$" + S3Object: + - property: Bucket + type: regex + value: "^(cdktoolkit-stagingbucket-.*|aws-nuke.*)$" + ConfigServiceConfigurationRecorder: + - "MainRecorder" + CloudFormationStack: + - property: Name + type: regex + value: "^(CDKToolkit)$" + - property: Name + type: regex + value: "^(PluginStack|NukeStack)*$" + IAMPolicy: + - property: Name + type: regex + value: "^(ConfigAccessPolicy|ResourceConfigurationCollectorPolicy|CloudFormationRefereeService|EC2CapacityReservationService|AwsSecurit.*AuditPolicy)$" + IAMRole: + - property: Name + type: regex + value: "^(AWSServiceRoleFor.*|Admin|ReadOnly|InternalAuditInternal|EC2CapacityReservationService|AccessAnalyzerTrustedService|AwsSecurit.*Audit|AWS.*Audit)$" + IAMRolePolicy: + - property: role:RoleName + type: regex + value: "^(AccessAnalyzerTrustedService|AwsSecurit.*Audit)$" + IAMRolePolicyAttachment: + - property: RoleName + type: regex + value: "^(Admin|ReadOnly|AWSServiceRoleFor.*|InternalAuditInternal|EC2CapacityReservationService|AWSVAPTAudit|AwsSecurit.*Audit)$" diff --git a/.tools/test/stacks/nuke/typescript/package.json b/.tools/test/stacks/nuke/typescript/package.json new file mode 100644 index 00000000000..8353504f81d --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/package.json @@ -0,0 +1,27 @@ +{ + "name": "account_nuker", + "version": "0.1.0", + "bin": { + "nuke_cleanser": "account_nuker.ts" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "22.5.4", + "aws-cdk": "2.164.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.2" + }, + "dependencies": { + "aws-cdk-lib": "^2.164.1", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + } +} diff --git a/.tools/test/stacks/nuke/typescript/run.sh b/.tools/test/stacks/nuke/typescript/run.sh new file mode 100755 index 00000000000..649d8857ba1 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/run.sh @@ -0,0 +1,15 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +#!/bin/sh + +# Get AWS account ID +AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +echo "AWS Account ID: $AWS_ACCOUNT_ID" + +# Copy the config file to /tmp and inject Account ID +echo "Copying & updating config file..." +cp /nuke_generic_config.yaml /tmp/nuke_config.yaml +sed -i "s/AWSACCOUNTID/$AWS_ACCOUNT_ID/g" /tmp/nuke_config.yaml + +echo "Running aws-nuke command:" +/usr/local/bin/aws-nuke run --config /tmp/nuke_config.yaml --force --max-wait-retries --no-dry-run 10 2>&1 diff --git a/.tools/test/stacks/nuke/typescript/tsconfig.json b/.tools/test/stacks/nuke/typescript/tsconfig.json new file mode 100644 index 00000000000..464ed774ba8 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020", "dom"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} From 8b17f937837b2625fc2b270d650c9ef6bbc6f786 Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Fri, 7 Feb 2025 06:44:22 -0800 Subject: [PATCH 028/144] Task: Add workflows for Dependabot auto-approve and auto-merge (#7232) --- .github/workflows/automerge-approved-prs.yml | 32 +++++++++++++++++ .github/workflows/dependabot-autoapprove.yml | 37 ++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 .github/workflows/automerge-approved-prs.yml create mode 100644 .github/workflows/dependabot-autoapprove.yml diff --git a/.github/workflows/automerge-approved-prs.yml b/.github/workflows/automerge-approved-prs.yml new file mode 100644 index 00000000000..e7ea47f3775 --- /dev/null +++ b/.github/workflows/automerge-approved-prs.yml @@ -0,0 +1,32 @@ +on: # yamllint disable-line rule:truthy + pull_request_review: + types: submitted + +jobs: + approved_pr: + name: Automerge approved PRs + permissions: + contents: write + pull-requests: write + id-token: write + if: ${{ github.event.review.state == 'approved' && github.repository == 'awsdocs/aws-doc-sdk-examples' && (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'MEMBER' || github.event.review.user.login == 'aws-sdk-osds') }} + runs-on: ubuntu-latest + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::206735643321:role/ConfigureAwsCredentialsPackageRole + role-duration-seconds: 900 + role-session-name: SecretsManagerFetch + - name: Get bot user token + uses: aws-actions/aws-secretsmanager-get-secrets@v2 + with: + parse-json-secrets: true + secret-ids: | + OSDS,arn:aws:secretsmanager:us-west-2:206735643321:secret:github-aws-sdk-osds-automation-gebs9n + - name: Enable PR automerge + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ env.OSDS_ACCESS_TOKEN }} diff --git a/.github/workflows/dependabot-autoapprove.yml b/.github/workflows/dependabot-autoapprove.yml new file mode 100644 index 00000000000..a4228da0627 --- /dev/null +++ b/.github/workflows/dependabot-autoapprove.yml @@ -0,0 +1,37 @@ +name: Dependabot auto-approve +on: pull_request # yamllint disable-line rule:truthy +permissions: + pull-requests: write + id-token: write +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'awsdocs/aws-doc-sdk-examples' }} + steps: + - name: Get Metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@v2 + - uses: actions/checkout@v4 + name: Clone repo + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::206735643321:role/ConfigureAwsCredentialsPackageRole + role-duration-seconds: 900 + - name: Get bot user token + uses: aws-actions/aws-secretsmanager-get-secrets@v2 + with: + parse-json-secrets: true + secret-ids: | + OSDS,arn:aws:secretsmanager:us-west-2:206735643321:secret:github-aws-sdk-osds-automation-gebs9n + - name: Approve PR if not already approved + run: | + gh pr checkout "$PR_URL" + if [ "$(gh pr status --json reviewDecision - q .currentBranch.reviewDecision)" != "APPROVED" ]; then + gh pr review "$PR_URL" --approve + else echo "PR already approved" + fi + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ env.OSDS_ACCESS_TOKEN }} From 0346da86c1b7c626d4a3f8d28e21aad3cebf8a94 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 09:45:04 -0500 Subject: [PATCH 029/144] updated the YAML file --- .../metadata/entityresolution_metadata.yaml | 96 +++++++++++++++++++ .../example/entity/HelloEntityResoultion.java | 79 +++++++++++++++ .../entity/scenario/EntityResActions.java | 1 - 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 400cd855972..f2a04ec572d 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -1,3 +1,99 @@ +entityresolution_HelloEntity: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_tag_resource.main + services: + entityresolution: {HelloEntity} +entityresolution_TagEntityResource: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_tag_resource.main + services: + entityresolution: {TagEntityResource} +entityresolution_CreateMatchingWork: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_create_matching_workflow.main + services: + entityresolution: {CreateMatchingWork} +entityresolution_CheckWorkflowStatus: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_check_matching_workflow.main + services: + entityresolution: {CheckWorkflowStatus} +entityresolution_StartMatchingJob: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_start_job.main + services: + entityresolution: {StartMatchingJob} +entityresolution_GetMatchingJob: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_get_job.main + services: + entityresolution: {GetMatchingJob} +entityresolution_DeleteMatchingWorkflow: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_delete_matching_workflow.main + services: + entityresolution: {DeleteMatchingWorkflow} +entityresolution_ListSchemaMappings: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_list_mappings.main + services: + entityresolution: {ListSchemaMappings} entityresolution_GetSchemaMapping: languages: Java: diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java new file mode 100644 index 00000000000..3ed975dd6b0 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity; + +import com.example.entity.scenario.EntityResScenario; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; +import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; +import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class HelloEntityResoultion { + + private static final Logger logger = LoggerFactory.getLogger(HelloEntityResoultion.class); + + private static EntityResolutionAsyncClient entityResolutionAsyncClient; + public static void main(String[] args) { + + + } + + public static EntityResolutionAsyncClient getResolutionAsyncClient() { + if (entityResolutionAsyncClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return entityResolutionAsyncClient; + } + + + public void ListSchemaMappings() { + ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() + .build(); + + ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); + + // Iterate through the pages of results + CompletableFuture future = paginator.subscribe(response -> { + response.schemaList().forEach(schemaMapping -> + logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) + ); + }); + + // Wait for the asynchronous operation to complete + future.join(); + } +} diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 1a605fe11f1..e4185ea01cc 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -79,7 +79,6 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { return entityResolutionAsyncClient; } - public static S3AsyncClient getS3AsyncClient() { if (s3AsyncClient == null) { /* From 8f1411d457232fa0696eb793d0727548b74e9061 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Fri, 7 Feb 2025 09:46:20 -0500 Subject: [PATCH 030/144] Checksums in S3 example updates (#7217) * Checksums in S3 example updates Added a SHA256 checksum to the multipart upload example. Added a new basic example showing the use of a checksum while doing a basic PutObject. Other cleanup. * New example for checksum with multipart upload Added a new multipart-upload example, based on the existing one from S3. This example merges some functions together and displays each part's checksum after uploading it. Also did minor cleanup on the S3 multipart upload example. --- .../s3/checksums/multipart/Package.swift | 40 +++ .../multipart/Sources/TransferError.swift | 31 +++ .../checksums/multipart/Sources/entry.swift | 243 ++++++++++++++++++ .../s3/checksums/upload/Package.swift | 40 +++ .../upload/Sources/TransferError.swift | 47 ++++ .../s3/checksums/upload/Sources/entry.swift | 107 ++++++++ .../Sources/TransferError.swift | 4 + .../s3/multipart-upload/Sources/entry.swift | 18 +- .../presigned-upload/TransferError.swift | 28 -- 9 files changed, 527 insertions(+), 31 deletions(-) create mode 100644 swift/example_code/s3/checksums/multipart/Package.swift create mode 100644 swift/example_code/s3/checksums/multipart/Sources/TransferError.swift create mode 100644 swift/example_code/s3/checksums/multipart/Sources/entry.swift create mode 100644 swift/example_code/s3/checksums/upload/Package.swift create mode 100644 swift/example_code/s3/checksums/upload/Sources/TransferError.swift create mode 100644 swift/example_code/s3/checksums/upload/Sources/entry.swift diff --git a/swift/example_code/s3/checksums/multipart/Package.swift b/swift/example_code/s3/checksums/multipart/Package.swift new file mode 100644 index 00000000000..cbe56d024a5 --- /dev/null +++ b/swift/example_code/s3/checksums/multipart/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "mpchecksums", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "mpchecksums", + dependencies: [ + .product(name: "AWSS3", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/s3/checksums/multipart/Sources/TransferError.swift b/swift/example_code/s3/checksums/multipart/Sources/TransferError.swift new file mode 100644 index 00000000000..9b413dc1511 --- /dev/null +++ b/swift/example_code/s3/checksums/multipart/Sources/TransferError.swift @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// Errors thrown by the example's functions. +enum TransferError: Error { + /// The checksum is missing or erroneous. + case checksumError + /// An error occurred when completing a multi-part upload to Amazon S3. + case multipartFinishError(_ message: String = "") + /// An error occurred when starting a multi-part upload to Amazon S3. + case multipartStartError + /// An error occurred while uploading a file to Amazon S3. + case uploadError(_ message: String = "") + /// An error occurred while reading the file's contents. + case readError + + var errorDescription: String? { + switch self { + case .checksumError: + return "The checksum is missing or incorrect" + case .multipartFinishError(message: let message): + return "An error occurred when completing a multi-part upload to Amazon S3. \(message)" + case .multipartStartError: + return "An error occurred when starting a multi-part upload to Amazon S3." + case .uploadError(message: let message): + return "An error occurred attempting to upload the file: \(message)" + case .readError: + return "An error occurred while reading the file data" + } + } +} diff --git a/swift/example_code/s3/checksums/multipart/Sources/entry.swift b/swift/example_code/s3/checksums/multipart/Sources/entry.swift new file mode 100644 index 00000000000..afa18749230 --- /dev/null +++ b/swift/example_code/s3/checksums/multipart/Sources/entry.swift @@ -0,0 +1,243 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +/// An example demonstrating how to perform multi-part uploads to Amazon S3 +/// using the AWS SDK for Swift. + +// snippet-start:[swift.s3.mp-checksums.imports] +import ArgumentParser +import AWSClientRuntime +import AWSS3 +import Foundation +import Smithy +// snippet-end:[swift.s3.mp-checksums.imports] + +// -MARK: - Async command line tool + +struct ExampleCommand: ParsableCommand { + // -MARK: Command arguments + @Option(help: "Path of local file to upload to Amazon S3") + var file: String + @Option(help: "Name of the Amazon S3 bucket to upload to") + var bucket: String + @Option(help: "Key name to give the file on Amazon S3") + var key: String? + @Option(help: "Name of the Amazon S3 Region to use") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "mpchecksums", + abstract: """ + This example shows how to use checksums with multi-part uploads. + """, + discussion: """ + """ + ) + + // -MARK: - File uploading + + // snippet-start:[swift.s3.mp-checksums.uploadfile] + /// Upload a file to Amazon S3. + /// + /// - Parameters: + /// - file: The path of the local file to upload to Amazon S3. + /// - bucket: The name of the bucket to upload the file into. + /// - key: The key (name) to give the object on Amazon S3. + /// + /// - Throws: Errors from `TransferError` + func uploadFile(file: String, bucket: String, key: String?) async throws { + let fileURL = URL(fileURLWithPath: file) + let fileName: String + + // If no key was provided, use the last component of the filename. + + if key == nil { + fileName = fileURL.lastPathComponent + } else { + fileName = key! + } + + // Create an Amazon S3 client in the desired Region. + + let config = try await S3Client.S3ClientConfiguration(region: region) + let s3Client = S3Client(config: config) + + print("Uploading file from \(fileURL.path) to \(bucket)/\(fileName).") + + let multiPartUploadOutput: CreateMultipartUploadOutput + + // First, create the multi-part upload, using SHA256 checksums. + + do { + multiPartUploadOutput = try await s3Client.createMultipartUpload( + input: CreateMultipartUploadInput( + bucket: bucket, + checksumAlgorithm: .sha256, + key: key + ) + ) + } catch { + throw TransferError.multipartStartError + } + + // Get the upload ID. This needs to be included with each part sent. + + guard let uploadID = multiPartUploadOutput.uploadId else { + throw TransferError.uploadError("Unable to get the upload ID") + } + + // Open a file handle and prepare to send the file in chunks. Each chunk + // is 5 MB, which is the minimum size allowed by Amazon S3. + + do { + let blockSize = Int(5 * 1024 * 1024) + let fileHandle = try FileHandle(forReadingFrom: fileURL) + let fileSize = try getFileSize(file: fileHandle) + let blockCount = Int(ceil(Double(fileSize) / Double(blockSize))) + var completedParts: [S3ClientTypes.CompletedPart] = [] + + // Upload the blocks one at as Amazon S3 object parts. + + print("Uploading...") + + for partNumber in 1...blockCount { + let data: Data + let startIndex = UInt64(partNumber - 1) * UInt64(blockSize) + + // Read the block from the file. + + data = try readFileBlock(file: fileHandle, startIndex: startIndex, size: blockSize) + + let uploadPartInput = UploadPartInput( + body: ByteStream.data(data), + bucket: bucket, + checksumAlgorithm: .sha256, + key: key, + partNumber: partNumber, + uploadId: uploadID + ) + + // Upload the part with a SHA256 checksum. + + do { + let uploadPartOutput = try await s3Client.uploadPart(input: uploadPartInput) + + guard let eTag = uploadPartOutput.eTag else { + throw TransferError.uploadError("Missing eTag") + } + guard let checksum = uploadPartOutput.checksumSHA256 else { + throw TransferError.checksumError + } + print("Part \(partNumber) checksum: \(checksum)") + + // Append the completed part description (including its + // checksum, ETag, and part number) to the + // `completedParts` array. + + completedParts.append( + S3ClientTypes.CompletedPart( + checksumSHA256: checksum, + eTag: eTag, + partNumber: partNumber + ) + ) + } catch { + throw TransferError.uploadError(error.localizedDescription) + } + } + + // Tell Amazon S3 that all parts have been uploaded. + + do { + let partInfo = S3ClientTypes.CompletedMultipartUpload(parts: completedParts) + let multiPartCompleteInput = CompleteMultipartUploadInput( + bucket: bucket, + key: key, + multipartUpload: partInfo, + uploadId: uploadID + ) + _ = try await s3Client.completeMultipartUpload(input: multiPartCompleteInput) + } catch { + throw TransferError.multipartFinishError(error.localizedDescription) + } + } catch { + throw TransferError.uploadError("Error uploading the file: \(error)") + } + + print("Done. Uploaded as \(fileName) in bucket \(bucket).") + } + // snippet-end:[swift.s3.mp-checksums.uploadfile] + + // -MARK: - File access + + /// Get the size of a file in bytes. + /// + /// - Parameter file: `FileHandle` identifying the file to return the size of. + /// + /// - Returns: The number of bytes in the file. + func getFileSize(file: FileHandle) throws -> UInt64 { + let fileSize: UInt64 + + // Get the total size of the file in bytes, then compute the number + // of blocks it will take to transfer the whole file. + + do { + try file.seekToEnd() + fileSize = try file.offset() + } catch { + throw TransferError.readError + } + return fileSize + } + + /// Read the specified range of bytes from a file and return them in a + /// new `Data` object. + /// + /// - Parameters: + /// - file: The `FileHandle` to read from. + /// - startIndex: The index of the first byte to read. + /// - size: The number of bytes to read. + /// + /// - Returns: A new `Data` object containing the specified range of bytes. + /// + /// - Throws: `TransferError.readError` if the read fails. + func readFileBlock(file: FileHandle, startIndex: UInt64, size: Int) throws -> Data { + file.seek(toFileOffset: startIndex) + do { + let data = try file.read(upToCount: size) + guard let data else { + throw TransferError.readError + } + return data + } catch { + throw TransferError.readError + } + } + + // -MARK: - Asynchronous main code + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + try await uploadFile(file: file, bucket: bucket, + key: key) + } +} + +// -MARK: - Entry point + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch let error as TransferError { + print("ERROR: \(error.errorDescription ?? "Unknown error")") + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/s3/checksums/upload/Package.swift b/swift/example_code/s3/checksums/upload/Package.swift new file mode 100644 index 00000000000..4fff4a7e98d --- /dev/null +++ b/swift/example_code/s3/checksums/upload/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "checksums", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "checksums", + dependencies: [ + .product(name: "AWSS3", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/s3/checksums/upload/Sources/TransferError.swift b/swift/example_code/s3/checksums/upload/Sources/TransferError.swift new file mode 100644 index 00000000000..f05b3640a39 --- /dev/null +++ b/swift/example_code/s3/checksums/upload/Sources/TransferError.swift @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// Errors thrown by the example's functions. +enum TransferError: Error { + /// The destination directory for a download is missing or inaccessible. + case directoryError + /// An error occurred while downloading a file from Amazon S3. + case downloadError(_ message: String = "") + /// An error occurred moving the file to its final destination. + case fileMoveError + /// An error occurred when completing a multi-part upload to Amazon S3. + case multipartFinishError + /// An error occurred when starting a multi-part upload to Amazon S3. + case multipartStartError + /// An error occurred while uploading a file to Amazon S3. + case uploadError(_ message: String = "") + /// An error occurred while reading the file's contents. + case readError + /// An error occurred while presigning the URL. + case signingError + /// An error occurred while writing the file's contents. + case writeError + + var errorDescription: String? { + switch self { + case .directoryError: + return "The destination directory could not be located or created" + case .downloadError(message: let message): + return "An error occurred attempting to download the file: \(message)" + case .fileMoveError: + return "The file couldn't be moved to the destination directory" + case .multipartFinishError: + return "An error occurred when completing a multi-part upload to Amazon S3." + case .multipartStartError: + return "An error occurred when starting a multi-part upload to Amazon S3." + case .uploadError(message: let message): + return "An error occurred attempting to upload the file: \(message)" + case .readError: + return "An error occurred while reading the file data" + case .signingError: + return "An error occurred while pre-signing the URL" + case .writeError: + return "An error occurred while writing the file data" + } + } +} diff --git a/swift/example_code/s3/checksums/upload/Sources/entry.swift b/swift/example_code/s3/checksums/upload/Sources/entry.swift new file mode 100644 index 00000000000..3442ff8ec1e --- /dev/null +++ b/swift/example_code/s3/checksums/upload/Sources/entry.swift @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +/// An example demonstrating how to configure checksums when uploading to +/// Amazon S3. + +// snippet-start:[swift.s3.checksums-upload.imports] +import ArgumentParser +import AWSClientRuntime +import AWSS3 +import Foundation +import Smithy +// snippet-end:[swift.s3.checksums-upload.imports] + +// -MARK: - Async command line tool + +struct ExampleCommand: ParsableCommand { + // -MARK: Command arguments + @Option(help: "Path of local file to upload to Amazon S3") + var source: String + @Option(help: "Name of the Amazon S3 bucket to upload to") + var bucket: String + @Option(help: "Destination file path within the bucket") + var dest: String? + @Option(help: "Name of the Amazon S3 Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "checksums", + abstract: """ + This example shows how to configure checksums when uploading to Amazon S3. + """, + discussion: """ + """ + ) + + // -MARK: - File upload + + func uploadFile(sourcePath: String, bucket: String, key: String?) async throws { + let fileURL = URL(fileURLWithPath: sourcePath) + let fileName: String + + // If no key was provided, use the last component of the filename. + + if key == nil { + fileName = fileURL.lastPathComponent + } else { + fileName = key! + } + + // Create an Amazon S3 client in the desired Region. + + let config = try await S3Client.S3ClientConfiguration(region: region) + let s3Client = S3Client(config: config) + + print("Uploading file from \(fileURL.path) to \(bucket)/\(fileName).") + + let fileData = try Data(contentsOf: fileURL) + let dataStream = ByteStream.data(fileData) + + // Use PutObject to send the file to Amazon S3. The checksum is + // specified by setting the `checksumAlgorithm` property. In this + // example, SHA256 is used. + + do { + // snippet-start:[swift.s3.checksums.upload-file] + _ = try await s3Client.putObject( + input: PutObjectInput( + body: dataStream, + bucket: bucket, + checksumAlgorithm: .sha256, + key: fileName + ) + ) + // snippet-end:[swift.s3.checksums.upload-file] + } catch { + throw TransferError.uploadError("Error uploading file: \(error.localizedDescription)") + } + print("Uploaded \(sourcePath) to \(bucket)/\(fileName).") + } + + // -MARK: - Asynchronous main code + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + try await uploadFile(sourcePath: source, bucket: bucket, key: dest) + } +} + +// -MARK: - Entry point + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch let error as TransferError { + print("ERROR: \(error.errorDescription ?? "Unknown error")") + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/s3/multipart-upload/Sources/TransferError.swift b/swift/example_code/s3/multipart-upload/Sources/TransferError.swift index afaa2101372..9b413dc1511 100644 --- a/swift/example_code/s3/multipart-upload/Sources/TransferError.swift +++ b/swift/example_code/s3/multipart-upload/Sources/TransferError.swift @@ -3,6 +3,8 @@ /// Errors thrown by the example's functions. enum TransferError: Error { + /// The checksum is missing or erroneous. + case checksumError /// An error occurred when completing a multi-part upload to Amazon S3. case multipartFinishError(_ message: String = "") /// An error occurred when starting a multi-part upload to Amazon S3. @@ -14,6 +16,8 @@ enum TransferError: Error { var errorDescription: String? { switch self { + case .checksumError: + return "The checksum is missing or incorrect" case .multipartFinishError(message: let message): return "An error occurred when completing a multi-part upload to Amazon S3. \(message)" case .multipartStartError: diff --git a/swift/example_code/s3/multipart-upload/Sources/entry.swift b/swift/example_code/s3/multipart-upload/Sources/entry.swift index 8054e26c5e9..e1854157147 100644 --- a/swift/example_code/s3/multipart-upload/Sources/entry.swift +++ b/swift/example_code/s3/multipart-upload/Sources/entry.swift @@ -6,7 +6,6 @@ // snippet-start:[swift.s3.multipart-upload.imports] import ArgumentParser -import AsyncHTTPClient import AWSClientRuntime import AWSS3 import Foundation @@ -38,6 +37,14 @@ struct ExampleCommand: ParsableCommand { // -MARK: - File uploading + /// Upload a file to Amazon S3. + /// + /// - Parameters: + /// - file: The path of the local file to upload to Amazon S3. + /// - bucket: The name of the bucket to upload the file into. + /// - key: The key (name) to give the object on Amazon S3. + /// + /// - Throws: Errors from `TransferError` func uploadFile(file: String, bucket: String, key: String?) async throws { let fileURL = URL(fileURLWithPath: file) let fileName: String @@ -175,13 +182,18 @@ struct ExampleCommand: ParsableCommand { uploadId: uploadID ) + // Upload the part. do { - let uploadPartOutput = try await client.uploadPart(input: uploadPartInput) + let uploadPartOutput = try await client.uploadPart(input: uploadPartInput) + guard let eTag = uploadPartOutput.eTag else { throw TransferError.uploadError("Missing eTag") } - return S3ClientTypes.CompletedPart(eTag: eTag, partNumber: partNumber) + return S3ClientTypes.CompletedPart( + eTag: eTag, + partNumber: partNumber + ) } catch { throw TransferError.uploadError(error.localizedDescription) } diff --git a/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift b/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift index f05b3640a39..f9bd8d2749d 100644 --- a/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift +++ b/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift @@ -3,45 +3,17 @@ /// Errors thrown by the example's functions. enum TransferError: Error { - /// The destination directory for a download is missing or inaccessible. - case directoryError - /// An error occurred while downloading a file from Amazon S3. - case downloadError(_ message: String = "") - /// An error occurred moving the file to its final destination. - case fileMoveError - /// An error occurred when completing a multi-part upload to Amazon S3. - case multipartFinishError - /// An error occurred when starting a multi-part upload to Amazon S3. - case multipartStartError /// An error occurred while uploading a file to Amazon S3. case uploadError(_ message: String = "") /// An error occurred while reading the file's contents. case readError - /// An error occurred while presigning the URL. - case signingError - /// An error occurred while writing the file's contents. - case writeError var errorDescription: String? { switch self { - case .directoryError: - return "The destination directory could not be located or created" - case .downloadError(message: let message): - return "An error occurred attempting to download the file: \(message)" - case .fileMoveError: - return "The file couldn't be moved to the destination directory" - case .multipartFinishError: - return "An error occurred when completing a multi-part upload to Amazon S3." - case .multipartStartError: - return "An error occurred when starting a multi-part upload to Amazon S3." case .uploadError(message: let message): return "An error occurred attempting to upload the file: \(message)" case .readError: return "An error occurred while reading the file data" - case .signingError: - return "An error occurred while pre-signing the URL" - case .writeError: - return "An error occurred while writing the file data" } } } From 4e14baa2a5e78feb3565c26c8667ad549408304d Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Fri, 7 Feb 2025 09:50:06 -0500 Subject: [PATCH 031/144] Clean up Transcribe example (#7208) --- .../transcribe-streaming/README.md | 2 +- .../transcribe-events/Sources/entry.swift | 41 +++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/swift/example_code/transcribe-streaming/README.md b/swift/example_code/transcribe-streaming/README.md index 5b39607d810..2c8f025d187 100644 --- a/swift/example_code/transcribe-streaming/README.md +++ b/swift/example_code/transcribe-streaming/README.md @@ -33,7 +33,7 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `swift Code excerpts that show you how to call individual service functions. -- [StartStreamTranscription](transcribe-events/Sources/entry.swift#L145) +- [StartStreamTranscription](transcribe-events/Sources/entry.swift#L132) ### Scenarios diff --git a/swift/example_code/transcribe-streaming/transcribe-events/Sources/entry.swift b/swift/example_code/transcribe-streaming/transcribe-events/Sources/entry.swift index 5bf9572df95..b1f68259793 100644 --- a/swift/example_code/transcribe-streaming/transcribe-events/Sources/entry.swift +++ b/swift/example_code/transcribe-streaming/transcribe-events/Sources/entry.swift @@ -126,26 +126,13 @@ struct ExampleCommand: ParsableCommand { /// Run the transcription process. /// /// - Throws: An error from `TranscribeError`. - func transcribe() async throws { - // Convert the value of the `--format` option into the Transcribe - // Streaming `MediaEncoding` type. - - let mediaEncoding: TranscribeStreamingClientTypes.MediaEncoding - switch format { - case .flac: - mediaEncoding = .flac - case .ogg: - mediaEncoding = .oggOpus - case .pcm: - mediaEncoding = .pcm - } - + func transcribe(encoding: TranscribeStreamingClientTypes.MediaEncoding) async throws { // Create the Transcribe Streaming client. // snippet-start:[swift.transcribe-streaming.StartStreamTranscription] let client = TranscribeStreamingClient( config: try await TranscribeStreamingClient.TranscribeStreamingClientConfiguration( - region: region + region: region ) ) @@ -155,7 +142,7 @@ struct ExampleCommand: ParsableCommand { input: StartStreamTranscriptionInput( audioStream: try await createAudioStream(), languageCode: TranscribeStreamingClientTypes.LanguageCode(rawValue: lang), - mediaEncoding: mediaEncoding, + mediaEncoding: encoding, mediaSampleRateHertz: sampleRate ) ) @@ -200,6 +187,26 @@ struct ExampleCommand: ParsableCommand { } } // snippet-end:[swift.transcribe-streaming] + + /// Convert the value of the `--format` command line option into the + /// corresponding Transcribe Streaming `MediaEncoding` type. + /// + /// - Returns: The `MediaEncoding` equivalent of the format specified on + /// the command line. + func getMediaEncoding() -> TranscribeStreamingClientTypes.MediaEncoding { + let mediaEncoding: TranscribeStreamingClientTypes.MediaEncoding + + switch format { + case .flac: + mediaEncoding = .flac + case .ogg: + mediaEncoding = .oggOpus + case .pcm: + mediaEncoding = .pcm + } + + return mediaEncoding + } } // -MARK: - Entry point @@ -212,7 +219,7 @@ struct Main { do { let command = try ExampleCommand.parse(args) - try await command.transcribe() + try await command.transcribe(encoding: command.getMediaEncoding()) } catch let error as TranscribeError { print("ERROR: \(error.errorDescription ?? "Unknown error")") } catch { From 189bb379d90e7b3bbe295c9748132a176b2af468 Mon Sep 17 00:00:00 2001 From: ford prior Date: Fri, 7 Feb 2025 11:02:31 -0500 Subject: [PATCH 032/144] Tools - Weathertop: Update plugin_stack.ts (#7222) --- .tools/test/stacks/plugin/typescript/plugin_stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tools/test/stacks/plugin/typescript/plugin_stack.ts b/.tools/test/stacks/plugin/typescript/plugin_stack.ts index 42357ad94fa..c9ac012fe97 100644 --- a/.tools/test/stacks/plugin/typescript/plugin_stack.ts +++ b/.tools/test/stacks/plugin/typescript/plugin_stack.ts @@ -114,7 +114,7 @@ class PluginStack extends cdk.Stack { type: "FARGATE", subnets: vpc.selectSubnets().subnetIds, securityGroupIds: [sg.securityGroupId], - maxvCpus: 1, + maxvCpus: 256, }, } ); From aec45c7179dbfa968decd90800c3063068ec5659 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 11:15:05 -0500 Subject: [PATCH 033/144] updated Hello example --- .../metadata/entityresolution_metadata.yaml | 10 +++++-- .../example/entity/HelloEntityResoultion.java | 29 ++++++++++++------- .../basics/entity_resolution/SPECIFICATION.md | 2 +- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index f2a04ec572d..5a7a6279b00 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -1,4 +1,8 @@ -entityresolution_HelloEntity: +entityresolution_Hello: + title: Hello &ERlong; + title_abbrev: Hello &ER; + synopsis: get started using &ER;. + category: Hello languages: Java: versions: @@ -7,9 +11,9 @@ entityresolution_HelloEntity: excerpts: - description: snippet_tags: - - entityres.java2_tag_resource.main + - entityres.java2_hello.main services: - entityresolution: {HelloEntity} + entityresolution: {listMatchingWorkflows} entityresolution_TagEntityResource: languages: Java: diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java index 3ed975dd6b0..78c57af049b 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java @@ -12,20 +12,28 @@ import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; +import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowResponse; +import software.amazon.awssdk.services.entityresolution.model.ListIdMappingJobsRequest; +import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsRequest; +import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsResponse; import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; +import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; +import software.amazon.awssdk.services.entityresolution.paginators.ListIdMappingJobsPublisher; +import software.amazon.awssdk.services.entityresolution.paginators.ListMatchingWorkflowsPublisher; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import java.time.Duration; import java.util.concurrent.CompletableFuture; +// snippet-start:[entityres.java2_hello.main] public class HelloEntityResoultion { private static final Logger logger = LoggerFactory.getLogger(HelloEntityResoultion.class); private static EntityResolutionAsyncClient entityResolutionAsyncClient; public static void main(String[] args) { - - + listMatchingWorkflows(); } public static EntityResolutionAsyncClient getResolutionAsyncClient() { @@ -57,19 +65,19 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { .build(); } return entityResolutionAsyncClient; - } + } - public void ListSchemaMappings() { - ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() - .build(); + public static void listMatchingWorkflows() { + ListMatchingWorkflowsRequest request = ListMatchingWorkflowsRequest.builder().build(); - ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); + ListMatchingWorkflowsPublisher paginator = + getResolutionAsyncClient().listMatchingWorkflowsPaginator(request); - // Iterate through the pages of results + // Iterate through the paginated results asynchronously CompletableFuture future = paginator.subscribe(response -> { - response.schemaList().forEach(schemaMapping -> - logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) + response.workflowSummaries().forEach(workflow -> + logger.info("Matching Workflow Name: " + workflow.workflowName()) ); }); @@ -77,3 +85,4 @@ public void ListSchemaMappings() { future.join(); } } +// snippet-end:[entityres.java2_hello.main] diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 0802942e9fd..990a899296a 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -7,7 +7,7 @@ This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database and a table, and two S3 buckets. A CDK script is provided to create these resources. ## Hello AWS Entity Resolution -This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listIdMappingJobsPaginator` to demonstrate how you can read through workflow job information. +This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listMatchingWorkflowsPaginator` to demonstrate how you can read through workflow information. ## Basics Scenario Program Flow The AWS Entity Resolution Basics scenario executes the following operations. From 82feea2ff1cddde5e9b21c319298a201f3dc5805 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 12:39:48 -0500 Subject: [PATCH 034/144] updated Hello example --- .../metadata/entityresolution_metadata.yaml | 29 ++++++++++++++++++- .../example/entity/HelloEntityResoultion.java | 28 +++++++++++------- .../entity/scenario/EntityResActions.java | 2 ++ .../entity/scenario/EntityResScenario.java | 5 ++-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 5a7a6279b00..a456a378044 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -121,4 +121,31 @@ entityresolution_CreateSchemaMapping: snippet_tags: - entityres.java2_create_schema.main services: - entityresolution: {CreateSchemaMapping} \ No newline at end of file + entityresolution: {CreateSchemaMapping} +entityresolution_Scenario: + synopsis_list: + - Create Schema Mapping. + - Create an &ERlong; workflow. + - Start the matching job for the workflow. + - Get details for the matching job. + - Get Schema Mapping. + - List all Schema Mappings. + - Tag the Schema Mapping resource. + - Delete the AWS Entity Resolution Workflow. + - Delete the &ERlong; Assets. + category: Basics + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + sdkguide: + excerpts: + - description: Run an interactive scenario demonstrating &ERlong; features. + snippet_tags: + - entityres.java2_scenario.main + - description: A wrapper class for &ITSW; SDK methods. + snippet_tags: + - iotsitewise.java2.actions.main + services: + iotsitewise: {} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java index 78c57af049b..f5dcbc3aeec 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java @@ -3,7 +3,6 @@ package com.example.entity; -import com.example.entity.scenario.EntityResScenario; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; @@ -12,21 +11,20 @@ import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; -import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowRequest; -import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowResponse; -import software.amazon.awssdk.services.entityresolution.model.ListIdMappingJobsRequest; import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsRequest; -import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsResponse; -import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; -import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; -import software.amazon.awssdk.services.entityresolution.paginators.ListIdMappingJobsPublisher; import software.amazon.awssdk.services.entityresolution.paginators.ListMatchingWorkflowsPublisher; -import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; - import java.time.Duration; import java.util.concurrent.CompletableFuture; // snippet-start:[entityres.java2_hello.main] +/** + * Before running this Java V2 code example, set up your development + * environment, including your credentials. + * + * For more information, see the following documentation topic: + * + * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html + */ public class HelloEntityResoultion { private static final Logger logger = LoggerFactory.getLogger(HelloEntityResoultion.class); @@ -65,9 +63,17 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { .build(); } return entityResolutionAsyncClient; - } + /** + * Lists all matching workflows using an asynchronous paginator. + *

+ * This method requests a paginated list of matching workflows from the + * AWS Entity Resolution service and logs the names of the retrieved workflows. + * It uses an asynchronous approach with a paginator and waits for the operation + * to complete using {@code CompletableFuture#join()}. + *

+ */ public static void listMatchingWorkflows() { ListMatchingWorkflowsRequest request = ListMatchingWorkflowsRequest.builder().build(); diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index e4185ea01cc..2c3cbb3691f 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +// snippet-start:[entityres.java2_actions.main] public class EntityResActions { private static final Logger logger = LoggerFactory.getLogger(EntityResActions.class); @@ -412,3 +413,4 @@ public boolean doesObjectExist(String bucketName) { } } } +// snippet-end:[entityres.java2_actions.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index cacb3763921..6099da2d4c5 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -11,6 +11,7 @@ import java.util.Scanner; import java.util.concurrent.CompletionException; +// snippet-start:[entityres.java2_scenario.main] public class EntityResScenario { private static final Logger logger = LoggerFactory.getLogger(EntityResScenario.class); public static final String DASHES = new String(new char[80]).replace("\0", "-"); @@ -289,5 +290,5 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } } - -} \ No newline at end of file +} +// snippet-end:[entityres.java2_scenario.main] \ No newline at end of file From dcc23ef83b8330b4e42a2f367a6e8ce7ace854c2 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 13:06:18 -0500 Subject: [PATCH 035/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index a456a378044..eeeaacfc20b 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -131,7 +131,7 @@ entityresolution_Scenario: - Get Schema Mapping. - List all Schema Mappings. - Tag the Schema Mapping resource. - - Delete the AWS Entity Resolution Workflow. + - Delete the Workflow. - Delete the &ERlong; Assets. category: Basics languages: From 4bb629cf400779d6b5bd7ed85916a647d26e6767 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 13:15:06 -0500 Subject: [PATCH 036/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index eeeaacfc20b..98fa364530d 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -26,7 +26,7 @@ entityresolution_TagEntityResource: - entityres.java2_tag_resource.main services: entityresolution: {TagEntityResource} -entityresolution_CreateMatchingWork: +entityresolution_CreateMatchingWorkflow: languages: Java: versions: @@ -37,7 +37,7 @@ entityresolution_CreateMatchingWork: snippet_tags: - entityres.java2_create_matching_workflow.main services: - entityresolution: {CreateMatchingWork} + entityresolution: {CreateMatchingWorkflow} entityresolution_CheckWorkflowStatus: languages: Java: From 97fce79255a62790095d0792f16bd4d00dcacbd8 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 13:22:01 -0500 Subject: [PATCH 037/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 98fa364530d..e9212ca8c64 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -131,7 +131,6 @@ entityresolution_Scenario: - Get Schema Mapping. - List all Schema Mappings. - Tag the Schema Mapping resource. - - Delete the Workflow. - Delete the &ERlong; Assets. category: Basics languages: From 36a1e7d72f8143132e8904b7f1a7b4abd87e9978 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 14:19:12 -0500 Subject: [PATCH 038/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index e9212ca8c64..d5f3ea8749d 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -147,4 +147,4 @@ entityresolution_Scenario: snippet_tags: - iotsitewise.java2.actions.main services: - iotsitewise: {} \ No newline at end of file + entityresolution: {} \ No newline at end of file From 70cc0963107b91f36bf956523bea7eaafdee7c21 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 14:31:45 -0500 Subject: [PATCH 039/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index d5f3ea8749d..79ba21a6022 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -143,7 +143,7 @@ entityresolution_Scenario: - description: Run an interactive scenario demonstrating &ERlong; features. snippet_tags: - entityres.java2_scenario.main - - description: A wrapper class for &ITSW; SDK methods. + - description: A wrapper class for &ERlong; SDK methods. snippet_tags: - iotsitewise.java2.actions.main services: From 3dddb00000c1178669070e7a10b27f71a0800a73 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 15:02:58 -0500 Subject: [PATCH 040/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 79ba21a6022..c9c23f58645 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -145,6 +145,6 @@ entityresolution_Scenario: - entityres.java2_scenario.main - description: A wrapper class for &ERlong; SDK methods. snippet_tags: - - iotsitewise.java2.actions.main + - entityres.java2_actions.main services: entityresolution: {} \ No newline at end of file From 6a93cf0a5b37b54c747d7a722d90da71ce572646 Mon Sep 17 00:00:00 2001 From: Chris Rees <34663864+AWSChris@users.noreply.github.com> Date: Tue, 11 Feb 2025 06:07:07 -0800 Subject: [PATCH 041/144] Python: InvokeFlow action Amazon Bedrock (#7216) --- .../bedrock-agent-runtime_metadata.yaml | 8 +++ .../bedrock-agent-runtime/README.md | 1 + .../bedrock_agent_runtime_wrapper.py | 56 ++++++++++++++++++- .../bedrock-agent-runtime/requirements.txt | 4 +- .../test_bedrock_agent_runtime_wrapper.py | 46 ++++++++++++++- .../bedrock_agent_runtime_stubber.py | 4 ++ 6 files changed, 115 insertions(+), 4 deletions(-) diff --git a/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml index c486a46b624..d667c22cb56 100644 --- a/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-agent-runtime_metadata.yaml @@ -30,6 +30,14 @@ bedrock-agent-runtime_InvokeFlow: - description: snippet_files: - javascriptv3/example_code/bedrock-agent-runtime/actions/invoke-flow.js + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-agent-runtime + excerpts: + - description: Invoke a flow. + snippet_tags: + - python.example_code.bedrock-agent-runtime.InvokeFlow services: bedrock-agent-runtime: {InvokeFlow} diff --git a/python/example_code/bedrock-agent-runtime/README.md b/python/example_code/bedrock-agent-runtime/README.md index f3c625a3bf4..f0657454c6b 100644 --- a/python/example_code/bedrock-agent-runtime/README.md +++ b/python/example_code/bedrock-agent-runtime/README.md @@ -46,6 +46,7 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. - [InvokeAgent](bedrock_agent_runtime_wrapper.py#L33) +- [InvokeFlow](bedrock_agent_runtime_wrapper.py#L71) diff --git a/python/example_code/bedrock-agent-runtime/bedrock_agent_runtime_wrapper.py b/python/example_code/bedrock-agent-runtime/bedrock_agent_runtime_wrapper.py index ab15b6a0de6..8651ca90cd5 100644 --- a/python/example_code/bedrock-agent-runtime/bedrock_agent_runtime_wrapper.py +++ b/python/example_code/bedrock-agent-runtime/bedrock_agent_runtime_wrapper.py @@ -68,4 +68,58 @@ def invoke_agent(self, agent_id, agent_alias_id, session_id, prompt): # snippet-end:[python.example_code.bedrock-agent-runtime.InvokeAgent] -# snippet-end:[python.example_code.bedrock-agent-runtime.BedrockAgentsRuntimeWrapper.class] + # snippet-start:[python.example_code.bedrock-agent-runtime.InvokeFlow] + def invoke_flow(self, flow_id, flow_alias_id, input_data, execution_id): + """ + Invoke an Amazon Bedrock flow and handle the response stream. + + Args: + param flow_id: The ID of the flow to invoke. + param flow_alias_id: The alias ID of the flow. + param input_data: Input data for the flow. + param execution_id: Execution ID for continuing a flow. Use the value None on first run. + + Return: Response from the flow. + """ + try: + + request_params = None + + if execution_id is None: + # Don't pass execution ID for first run. + request_params = { + "flowIdentifier": flow_id, + "flowAliasIdentifier": flow_alias_id, + "inputs": input_data, + "enableTrace": True + } + else: + request_params = { + "flowIdentifier": flow_id, + "flowAliasIdentifier": flow_alias_id, + "executionId": execution_id, + "inputs": input_data, + "enableTrace": True + } + + response = self.agents_runtime_client.invoke_flow(**request_params) + + if "executionId" not in request_params: + execution_id = response['executionId'] + + result = "" + + # Get the streaming response + for event in response['responseStream']: + result = result + str(event) + '\n' + print(result) + + except ClientError as e: + logger.error("Couldn't invoke flow %s.", {e}) + raise + + return result + + # snippet-end:[python.example_code.bedrock-agent-runtime.InvokeFlow] + +# snippet-end:[python.example_code.bedrock-agent-runtime.BedrockAgentsRuntimeWrapper.class] \ No newline at end of file diff --git a/python/example_code/bedrock-agent-runtime/requirements.txt b/python/example_code/bedrock-agent-runtime/requirements.txt index db118baeb60..bfec492f1b4 100644 --- a/python/example_code/bedrock-agent-runtime/requirements.txt +++ b/python/example_code/bedrock-agent-runtime/requirements.txt @@ -1,5 +1,5 @@ -boto3==1.33.8 -botocore==1.33.8 +boto3==1.36.13 +botocore==1.36.13 colorama==0.4.6 iniconfig==2.0.0 jmespath==1.0.1 diff --git a/python/example_code/bedrock-agent-runtime/test/test_bedrock_agent_runtime_wrapper.py b/python/example_code/bedrock-agent-runtime/test/test_bedrock_agent_runtime_wrapper.py index 36382439560..ee3181c3ec1 100644 --- a/python/example_code/bedrock-agent-runtime/test/test_bedrock_agent_runtime_wrapper.py +++ b/python/example_code/bedrock-agent-runtime/test/test_bedrock_agent_runtime_wrapper.py @@ -40,8 +40,52 @@ async def test_invoke_agent(make_stubber, error_code): if error_code is None: wrapper.invoke_agent(agent_id, agent_alias_id, session_id, prompt) else: - with pytest.raises(ClientError): + with pytest.raises(ClientError) as exc_info: async for _ in wrapper.invoke_agent( agent_id, agent_alias_id, session_id, prompt ): assert exc_info.value.response["Error"]["Code"] == error_code + +@pytest.mark.asyncio +@pytest.mark.parametrize("error_code", [None, "ClientError"]) +async def test_invoke_flow(make_stubber, error_code): + runtime_client = boto3.client( + service_name="bedrock-agent-runtime", region_name="us-east-1" + ) + stubber = make_stubber(runtime_client) + wrapper = BedrockAgentRuntimeWrapper(runtime_client) + + flow_id = "FAKE_AGENT_ID" + flow_alias_id = "FAKE_AGENT_ALIAS_ID" + execution_id = "FAKE_SESSION_ID" + + + inputs = [ + { + "content": { + "document": "hello!" + }, + "nodeName": "FlowInputNode", + "nodeOutputName": "document" + } + ] + + expected_params = { + "enableTrace" : True, + "flowIdentifier": flow_id, + "flowAliasIdentifier": flow_alias_id, + "executionId": execution_id, + "inputs": inputs + } + response = {"responseStream": {}, "executionId": execution_id} + + stubber.stub_invoke_flow(expected_params, response, error_code=error_code) + + if error_code is None: + result = wrapper.invoke_flow(flow_id, flow_alias_id, inputs, execution_id) + assert result is not None + else: + with pytest.raises(ClientError) as exc_info: + wrapper.invoke_flow(flow_id, flow_alias_id, inputs, execution_id) + + assert exc_info.value.response["Error"]["Code"] == error_code diff --git a/python/test_tools/bedrock_agent_runtime_stubber.py b/python/test_tools/bedrock_agent_runtime_stubber.py index 01baec209d8..cc62e9230f9 100644 --- a/python/test_tools/bedrock_agent_runtime_stubber.py +++ b/python/test_tools/bedrock_agent_runtime_stubber.py @@ -31,3 +31,7 @@ def stub_invoke_agent(self, expected_params, response, error_code=None): self._stub_bifurcator( "invoke_agent", expected_params, response, error_code=error_code ) + def stub_invoke_flow(self, expected_params, response, error_code=None): + self._stub_bifurcator( + "invoke_flow", expected_params, response, error_code=error_code + ) \ No newline at end of file From bb39ac7dde017726f9655207f259f46372703793 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Tue, 11 Feb 2025 09:08:40 -0500 Subject: [PATCH 042/144] SNS examples for Swift SDK (#7233) --- .doc_gen/metadata/sns_metadata.yaml | 57 ++++++++++ .../sns/CreateTopic/Package.swift | 40 +++++++ .../sns/CreateTopic/Sources/entry.swift | 60 ++++++++++ .../sns/DeleteTopic/Package.swift | 40 +++++++ .../sns/DeleteTopic/Sources/entry.swift | 55 ++++++++++ swift/example_code/sns/Publish/Package.swift | 40 +++++++ .../sns/Publish/Sources/entry.swift | 64 +++++++++++ swift/example_code/sns/README.md | 103 ++++++++++++++++++ .../sns/SubscribeEmail/Package.swift | 40 +++++++ .../sns/SubscribeEmail/Sources/entry.swift | 67 ++++++++++++ .../sns/SubscribeSMS/Package.swift | 40 +++++++ .../sns/SubscribeSMS/Sources/entry.swift | 67 ++++++++++++ .../sns/Unsubscribe/Package.swift | 40 +++++++ .../sns/Unsubscribe/Sources/entry.swift | 57 ++++++++++ swift/example_code/sns/basics/Package.swift | 42 +++++++ .../sns/basics/Sources/entry.swift | 79 ++++++++++++++ 16 files changed, 891 insertions(+) create mode 100644 swift/example_code/sns/CreateTopic/Package.swift create mode 100644 swift/example_code/sns/CreateTopic/Sources/entry.swift create mode 100644 swift/example_code/sns/DeleteTopic/Package.swift create mode 100644 swift/example_code/sns/DeleteTopic/Sources/entry.swift create mode 100644 swift/example_code/sns/Publish/Package.swift create mode 100644 swift/example_code/sns/Publish/Sources/entry.swift create mode 100644 swift/example_code/sns/README.md create mode 100644 swift/example_code/sns/SubscribeEmail/Package.swift create mode 100644 swift/example_code/sns/SubscribeEmail/Sources/entry.swift create mode 100644 swift/example_code/sns/SubscribeSMS/Package.swift create mode 100644 swift/example_code/sns/SubscribeSMS/Sources/entry.swift create mode 100644 swift/example_code/sns/Unsubscribe/Package.swift create mode 100644 swift/example_code/sns/Unsubscribe/Sources/entry.swift create mode 100644 swift/example_code/sns/basics/Package.swift create mode 100644 swift/example_code/sns/basics/Sources/entry.swift diff --git a/.doc_gen/metadata/sns_metadata.yaml b/.doc_gen/metadata/sns_metadata.yaml index 3b2cb00107d..82a11953148 100644 --- a/.doc_gen/metadata/sns_metadata.yaml +++ b/.doc_gen/metadata/sns_metadata.yaml @@ -56,6 +56,17 @@ sns_Hello: - description: Initialize an SNS client and and list topics in your account. snippet_tags: - javascript.v3.sns.hello + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns/basics + excerpts: + - description: The Package.swift file. + snippet_tags: + - swift.sns.basics.package + - description: The main Swift program. + snippet_tags: + - swift.sns.basics.hello services: sns: {ListTopics} sns_GetTopicAttributes: @@ -294,6 +305,13 @@ sns_ListTopics: excerpts: - snippet_tags: - sns.rust.list-topics + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns/basics + excerpts: + - snippet_tags: + - swift.sns.ListTopics SAP ABAP: versions: - sdk_version: 1 @@ -525,6 +543,13 @@ sns_CreateTopic: excerpts: - snippet_tags: - sns.rust.create-topic + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns + excerpts: + - snippet_tags: + - swift.sns.CreateTopic SAP ABAP: versions: - sdk_version: 1 @@ -607,6 +632,13 @@ sns_DeleteTopic: - snippet_tags: - python.example_code.sns.SnsWrapper - python.example_code.sns.DeleteTopic + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns + excerpts: + - snippet_tags: + - swift.sns.DeleteTopic SAP ABAP: versions: - sdk_version: 1 @@ -745,6 +777,13 @@ sns_Publish: excerpts: - snippet_tags: - sns.rust.sns-hello-world + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns + excerpts: + - snippet_tags: + - swift.sns.Publish SAP ABAP: versions: - sdk_version: 1 @@ -1067,6 +1106,17 @@ sns_Subscribe: - description: Subscribe an email address to a topic. snippet_tags: - sns.rust.sns-hello-world + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns + excerpts: + - description: Subscribe an email address to a topic. + snippet_tags: + - swift.sns.SubscribeEmail + - description: Subscribe a phone number to a topic to receive notifications by SMS. + snippet_tags: + - swift.sns.SubscribeSMS SAP ABAP: versions: - sdk_version: 1 @@ -1140,6 +1190,13 @@ sns_Unsubscribe: - snippet_tags: - python.example_code.sns.SnsWrapper - python.example_code.sns.Unsubscribe + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sns + excerpts: + - snippet_tags: + - swift.sns.Unsubscribe SAP ABAP: versions: - sdk_version: 1 diff --git a/swift/example_code/sns/CreateTopic/Package.swift b/swift/example_code/sns/CreateTopic/Package.swift new file mode 100644 index 00000000000..780a718433e --- /dev/null +++ b/swift/example_code/sns/CreateTopic/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "createtopic", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "createtopic", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sns/CreateTopic/Sources/entry.swift b/swift/example_code/sns/CreateTopic/Sources/entry.swift new file mode 100644 index 00000000000..636acc2227c --- /dev/null +++ b/swift/example_code/sns/CreateTopic/Sources/entry.swift @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Notification +// Service (SNS) client to create a new topic. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "Name to give the new Amazon SNS topic") + var name: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "createtopic", + abstract: """ + This example shows how to create an Amazon SNS topic. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.CreateTopic] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + let output = try await snsClient.createTopic( + input: CreateTopicInput(name: name) + ) + + guard let arn = output.topicArn else { + print("No topic ARN returned by Amazon SNS.") + return + } + // snippet-end:[swift.sns.CreateTopic] + + print("New topic created with ARN: \(arn)") + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sns/DeleteTopic/Package.swift b/swift/example_code/sns/DeleteTopic/Package.swift new file mode 100644 index 00000000000..2ce4a387ecd --- /dev/null +++ b/swift/example_code/sns/DeleteTopic/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "deletetopic", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "deletetopic", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sns/DeleteTopic/Sources/entry.swift b/swift/example_code/sns/DeleteTopic/Sources/entry.swift new file mode 100644 index 00000000000..bd8ba43e54f --- /dev/null +++ b/swift/example_code/sns/DeleteTopic/Sources/entry.swift @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Notification +// Service (SNS) client to delete a topic. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The ARN of the Amazon SNS topic to delete") + var arn: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "deletetopic", + abstract: """ + This example shows how to delete an Amazon SNS topic. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.DeleteTopic] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + _ = try await snsClient.deleteTopic( + input: DeleteTopicInput(topicArn: arn) + ) + // snippet-end:[swift.sns.DeleteTopic] + + print("Topic deleted.") + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sns/Publish/Package.swift b/swift/example_code/sns/Publish/Package.swift new file mode 100644 index 00000000000..b338d31b867 --- /dev/null +++ b/swift/example_code/sns/Publish/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "publish", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "publish", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sns/Publish/Sources/entry.swift b/swift/example_code/sns/Publish/Sources/entry.swift new file mode 100644 index 00000000000..cfc582cbb44 --- /dev/null +++ b/swift/example_code/sns/Publish/Sources/entry.swift @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to publish a message to an Amazon SNS topic. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The ARN of the Amazon SNS topic to publish to") + var arn: String + @Argument(help: "The message to publish to the topic") + var message: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "publish", + abstract: """ + This example shows how to publish a message to an Amazon SNS topic. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.Publish] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + let output = try await snsClient.publish( + input: PublishInput( + message: message, + topicArn: arn + ) + ) + + guard let messageId = output.messageId else { + print("No message ID received from Amazon SNS.") + return + } + + print("Published message with ID \(messageId)") + // snippet-end:[swift.sns.Publish] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sns/README.md b/swift/example_code/sns/README.md new file mode 100644 index 00000000000..ba494a4ca23 --- /dev/null +++ b/swift/example_code/sns/README.md @@ -0,0 +1,103 @@ +# Amazon SNS code examples for the SDK for Swift + +## Overview + +Shows how to use the AWS SDK for Swift to work with Amazon Simple Notification Service (Amazon SNS). + + + + +_Amazon SNS is a web service that enables applications, end-users, and devices to instantly send and receive notifications from the cloud._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `swift` folder. + + + + + +### Get started + +- [Hello Amazon SNS](basics/Package.swift#L8) (`ListTopics`) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreateTopic](CreateTopic/Sources/entry.swift#L29) +- [DeleteTopic](DeleteTopic/Sources/entry.swift#L29) +- [ListTopics](basics/Sources/entry.swift#L28) +- [Publish](Publish/Sources/entry.swift#L30) +- [Subscribe](SubscribeEmail/Sources/entry.swift#L31) +- [Unsubscribe](Unsubscribe/Sources/entry.swift#L29) + + + + + +## Run the examples + +### Instructions + +To build any of these examples from a terminal window, navigate into its +directory, then use the following command: + +``` +$ swift build +``` + +To build one of these examples in Xcode, navigate to the example's directory +(such as the `ListUsers` directory, to build that example). Then type `xed.` +to open the example directory in Xcode. You can then use standard Xcode build +and run commands. + + + + +#### Hello Amazon SNS + +This example shows you how to get started using Amazon SNS. + + + +### 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 `swift` folder. + + + + + + +## Additional resources + +- [Amazon SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/welcome.html) +- [Amazon SNS API Reference](https://docs.aws.amazon.com/sns/latest/api/welcome.html) +- [SDK for Swift Amazon SNS reference](https://sdk.amazonaws.com/swift/api/awssns/latest/documentation/awssns) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/swift/example_code/sns/SubscribeEmail/Package.swift b/swift/example_code/sns/SubscribeEmail/Package.swift new file mode 100644 index 00000000000..025dd7f225c --- /dev/null +++ b/swift/example_code/sns/SubscribeEmail/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "subscribe-email", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "subscribe-email", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sns/SubscribeEmail/Sources/entry.swift b/swift/example_code/sns/SubscribeEmail/Sources/entry.swift new file mode 100644 index 00000000000..298819f629c --- /dev/null +++ b/swift/example_code/sns/SubscribeEmail/Sources/entry.swift @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to subscribe an email address to an Amazon Simple +// Notification Service (SNS) topic. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The ARN of the Amazon SNS topic to subscribe to") + var arn: String + @Argument(help: "The email address to subscribe to the topic") + var email: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "subscribe-email", + abstract: """ + Subscribes an email address to an Amazon SNS topic. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.SubscribeEmail] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + let output = try await snsClient.subscribe( + input: SubscribeInput( + endpoint: email, + protocol: "email", + returnSubscriptionArn: true, + topicArn: arn + ) + ) + + guard let subscriptionArn = output.subscriptionArn else { + print("No subscription ARN received from Amazon SNS.") + return + } + + print("Subscription \(subscriptionArn) created.") + // snippet-end:[swift.sns.SubscribeEmail] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sns/SubscribeSMS/Package.swift b/swift/example_code/sns/SubscribeSMS/Package.swift new file mode 100644 index 00000000000..fcc48c7a38f --- /dev/null +++ b/swift/example_code/sns/SubscribeSMS/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "subscribe-sms", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "subscribe-sms", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sns/SubscribeSMS/Sources/entry.swift b/swift/example_code/sns/SubscribeSMS/Sources/entry.swift new file mode 100644 index 00000000000..5a1b521e474 --- /dev/null +++ b/swift/example_code/sns/SubscribeSMS/Sources/entry.swift @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to subscribe an email address to an Amazon Simple +// Notification Service (SNS) topic. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The ARN of the Amazon SNS topic to subscribe to") + var arn: String + @Argument(help: "The phone number to subscribe to the topic") + var phone: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "subscribe-sms", + abstract: """ + Subscribes a phone number to receive text messages from an Amazon SNS topic. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.SubscribeSMS] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + let output = try await snsClient.subscribe( + input: SubscribeInput( + endpoint: phone, + protocol: "sms", + returnSubscriptionArn: true, + topicArn: arn + ) + ) + + guard let subscriptionArn = output.subscriptionArn else { + print("No subscription ARN received from Amazon SNS.") + return + } + + print("Subscription \(subscriptionArn) created.") + // snippet-end:[swift.sns.SubscribeSMS] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sns/Unsubscribe/Package.swift b/swift/example_code/sns/Unsubscribe/Package.swift new file mode 100644 index 00000000000..45532bd83a7 --- /dev/null +++ b/swift/example_code/sns/Unsubscribe/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "unsubscribe", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "unsubscribe", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sns/Unsubscribe/Sources/entry.swift b/swift/example_code/sns/Unsubscribe/Sources/entry.swift new file mode 100644 index 00000000000..b7ba9bc81ab --- /dev/null +++ b/swift/example_code/sns/Unsubscribe/Sources/entry.swift @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to unsubscribe a subscriber from an Amazon Simple +// Notification Service (SNS) topic. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The ARN of the subscriber to unsubscribe") + var arn: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "unsubscribe", + abstract: """ + Unsubscribe a subscriber from an Amazon SNS topic. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.Unsubscribe] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + _ = try await snsClient.unsubscribe( + input: UnsubscribeInput( + subscriptionArn: arn + ) + ) + + print("Unsubscribed.") + // snippet-end:[swift.sns.Unsubscribe] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sns/basics/Package.swift b/swift/example_code/sns/basics/Package.swift new file mode 100644 index 00000000000..d8a60c12d4e --- /dev/null +++ b/swift/example_code/sns/basics/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +// snippet-start:[swift.sns.basics.package] +import PackageDescription + +let package = Package( + name: "sns-basics", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "sns-basics", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) +// snippet-end:[swift.sns.basics.package] diff --git a/swift/example_code/sns/basics/Sources/entry.swift b/swift/example_code/sns/basics/Sources/entry.swift new file mode 100644 index 00000000000..446fa749c8d --- /dev/null +++ b/swift/example_code/sns/basics/Sources/entry.swift @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Notification +// Service client to list your available Amazon SNS topics. + +// snippet-start:[swift.sns.basics.hello] +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "sns-basics", + abstract: """ + This example shows how to list all of your available Amazon SNS topics. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sns.ListTopics] + let config = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: config) + + var topics: [String] = [] + let outputPages = snsClient.listTopicsPaginated( + input: ListTopicsInput() + ) + + // Each time a page of results arrives, process its contents. + + for try await output in outputPages { + guard let topicList = output.topics else { + print("Unable to get a page of Amazon SNS topics.") + return + } + + // Iterate over the topics listed on this page, adding their ARNs + // to the `topics` array. + + for topic in topicList { + guard let arn = topic.topicArn else { + print("Topic has no ARN.") + return + } + topics.append(arn) + } + } + // snippet-end:[swift.sns.ListTopics] + + print("You have \(topics.count) topics:") + for topic in topics { + print(" \(topic)") + } + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} +// snippet-end:[swift.sns.basics.hello] From dea724958b4544fcf8d4c8be182508a445ea497b Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:36:02 -0600 Subject: [PATCH 043/144] .NET v4: Cognito Folder for v4 preview (#7236) --- dotnetv3/Cognito/README.md | 2 +- dotnetv4/Aurora/Actions/AuroraWrapper.cs | 2 +- .../Cognito/Actions/CognitoActions.csproj | 17 + dotnetv4/Cognito/Actions/CognitoWrapper.cs | 347 ++++++++++++++++++ dotnetv4/Cognito/Actions/HelloCognito.cs | 64 ++++ dotnetv4/Cognito/Actions/Usings.cs | 13 + dotnetv4/Cognito/CognitoExamples.sln | 48 +++ dotnetv4/Cognito/README.md | 138 +++++++ .../Scenarios/Cognito_Basics/CognitoBasics.cs | 160 ++++++++ .../Cognito_Basics/CognitoBasics.csproj | 29 ++ .../Scenarios/Cognito_Basics/UIMethods.cs | 44 +++ .../Scenarios/Cognito_Basics/Usings.cs | 14 + .../Scenarios/Cognito_Basics/settings.json | 9 + dotnetv4/Cognito/Tests/CognitoBasicsTests.cs | 198 ++++++++++ dotnetv4/Cognito/Tests/CognitoTests.csproj | 38 ++ dotnetv4/Cognito/Tests/Usings.cs | 8 + dotnetv4/Cognito/Tests/testsettings.json | 8 + dotnetv4/DotNetV4Examples.sln | 26 ++ 18 files changed, 1163 insertions(+), 2 deletions(-) create mode 100644 dotnetv4/Cognito/Actions/CognitoActions.csproj create mode 100644 dotnetv4/Cognito/Actions/CognitoWrapper.cs create mode 100644 dotnetv4/Cognito/Actions/HelloCognito.cs create mode 100644 dotnetv4/Cognito/Actions/Usings.cs create mode 100644 dotnetv4/Cognito/CognitoExamples.sln create mode 100644 dotnetv4/Cognito/README.md create mode 100644 dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.cs create mode 100644 dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.csproj create mode 100644 dotnetv4/Cognito/Scenarios/Cognito_Basics/UIMethods.cs create mode 100644 dotnetv4/Cognito/Scenarios/Cognito_Basics/Usings.cs create mode 100644 dotnetv4/Cognito/Scenarios/Cognito_Basics/settings.json create mode 100644 dotnetv4/Cognito/Tests/CognitoBasicsTests.cs create mode 100644 dotnetv4/Cognito/Tests/CognitoTests.csproj create mode 100644 dotnetv4/Cognito/Tests/Usings.cs create mode 100644 dotnetv4/Cognito/Tests/testsettings.json diff --git a/dotnetv3/Cognito/README.md b/dotnetv3/Cognito/README.md index eb9c4e7777b..9158cda1f7e 100644 --- a/dotnetv3/Cognito/README.md +++ b/dotnetv3/Cognito/README.md @@ -34,7 +34,7 @@ These examples also require the following resources: To create these resources, run the AWS CloudFormation script in the -[resources/cdk/cognito_scenario_user_pool_with_mfa](../../../resources/cdk/cognito_scenario_user_pool_with_mfa) +[resources/cdk/cognito_scenario_user_pool_with_mfa](../../resources/cdk/cognito_scenario_user_pool_with_mfa) folder. This script outputs a user pool ID and a client ID that you can use to run the scenario. diff --git a/dotnetv4/Aurora/Actions/AuroraWrapper.cs b/dotnetv4/Aurora/Actions/AuroraWrapper.cs index 18c7646cc9e..9f469df9fb2 100644 --- a/dotnetv4/Aurora/Actions/AuroraWrapper.cs +++ b/dotnetv4/Aurora/Actions/AuroraWrapper.cs @@ -124,7 +124,7 @@ public async Task ModifyIntegerParametersInGroupAsync(string groupName, { foreach (var p in parameters) { - if (p.IsModifiable.Value && p.DataType == "integer") + if (p.IsModifiable.GetValueOrDefault() && p.DataType == "integer") { while (newValue == 0) { diff --git a/dotnetv4/Cognito/Actions/CognitoActions.csproj b/dotnetv4/Cognito/Actions/CognitoActions.csproj new file mode 100644 index 00000000000..653035419c0 --- /dev/null +++ b/dotnetv4/Cognito/Actions/CognitoActions.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/dotnetv4/Cognito/Actions/CognitoWrapper.cs b/dotnetv4/Cognito/Actions/CognitoWrapper.cs new file mode 100644 index 00000000000..188a6bb1cd2 --- /dev/null +++ b/dotnetv4/Cognito/Actions/CognitoWrapper.cs @@ -0,0 +1,347 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Cognito.dotnetv4.CognitoWrapper] +using System.Net; + +namespace CognitoActions; + +/// +/// Methods to perform Amazon Cognito Identity Provider actions. +/// +public class CognitoWrapper +{ + private readonly IAmazonCognitoIdentityProvider _cognitoService; + + /// + /// Constructor for the wrapper class containing Amazon Cognito actions. + /// + /// The Amazon Cognito client object. + public CognitoWrapper(IAmazonCognitoIdentityProvider cognitoService) + { + _cognitoService = cognitoService; + } + + // snippet-start:[Cognito.dotnetv4.ListUserPools] + /// + /// List the Amazon Cognito user pools for an account. + /// + /// A list of UserPoolDescriptionType objects. + public async Task> ListUserPoolsAsync() + { + var userPools = new List(); + + var userPoolsPaginator = _cognitoService.Paginators.ListUserPools(new ListUserPoolsRequest()); + + await foreach (var response in userPoolsPaginator.Responses) + { + userPools.AddRange(response.UserPools); + } + + return userPools; + } + + // snippet-end:[Cognito.dotnetv4.ListUserPools] + + // snippet-start:[Cognito.dotnetv4.ListUsers] + /// + /// Get a list of users for the Amazon Cognito user pool. + /// + /// The user pool ID. + /// A list of users. + public async Task> ListUsersAsync(string userPoolId) + { + var request = new ListUsersRequest + { + UserPoolId = userPoolId + }; + + var users = new List(); + + var usersPaginator = _cognitoService.Paginators.ListUsers(request); + await foreach (var response in usersPaginator.Responses) + { + users.AddRange(response.Users); + } + + return users; + } + + // snippet-end:[Cognito.dotnetv4.ListUsers] + + // snippet-start:[Cognito.dotnetv4.AdminRespondToAuthChallenge] + /// + /// Respond to an admin authentication challenge. + /// + /// The name of the user. + /// The client ID. + /// The multi-factor authentication code. + /// The current application session. + /// The user pool ID. + /// The result of the authentication response. + public async Task AdminRespondToAuthChallengeAsync( + string userName, + string clientId, + string mfaCode, + string session, + string userPoolId) + { + Console.WriteLine("SOFTWARE_TOKEN_MFA challenge is generated"); + + var challengeResponses = new Dictionary(); + challengeResponses.Add("USERNAME", userName); + challengeResponses.Add("SOFTWARE_TOKEN_MFA_CODE", mfaCode); + + var respondToAuthChallengeRequest = new AdminRespondToAuthChallengeRequest + { + ChallengeName = ChallengeNameType.SOFTWARE_TOKEN_MFA, + ClientId = clientId, + ChallengeResponses = challengeResponses, + Session = session, + UserPoolId = userPoolId, + }; + + var response = await _cognitoService.AdminRespondToAuthChallengeAsync(respondToAuthChallengeRequest); + Console.WriteLine($"Response to Authentication {response.AuthenticationResult.TokenType}"); + return response.AuthenticationResult; + } + + // snippet-end:[Cognito.dotnetv4.AdminRespondToAuthChallenge] + + // snippet-start:[Cognito.dotnetv4.VerifySoftwareToken] + /// + /// Verify the TOTP and register for MFA. + /// + /// The name of the session. + /// The MFA code. + /// The status of the software token. + public async Task VerifySoftwareTokenAsync(string session, string code) + { + var tokenRequest = new VerifySoftwareTokenRequest + { + UserCode = code, + Session = session, + }; + + var verifyResponse = await _cognitoService.VerifySoftwareTokenAsync(tokenRequest); + + return verifyResponse.Status; + } + + // snippet-end:[Cognito.dotnetv4.VerifySoftwareToken] + + // snippet-start:[Cognito.dotnetv4.AssociateSoftwareToken] + /// + /// Get an MFA token to authenticate the user with the authenticator. + /// + /// The session name. + /// The session name. + public async Task AssociateSoftwareTokenAsync(string session) + { + var softwareTokenRequest = new AssociateSoftwareTokenRequest + { + Session = session, + }; + + var tokenResponse = await _cognitoService.AssociateSoftwareTokenAsync(softwareTokenRequest); + var secretCode = tokenResponse.SecretCode; + + Console.WriteLine($"Use the following secret code to set up the authenticator: {secretCode}"); + + return tokenResponse.Session; + } + + // snippet-end:[Cognito.dotnetv4.AssociateSoftwareToken] + + // snippet-start:[Cognito.dotnetv4.AdminInitiateAuth] + /// + /// Initiate an admin auth request. + /// + /// The client ID to use. + /// The ID of the user pool. + /// The username to authenticate. + /// The user's password. + /// The session to use in challenge-response. + public async Task AdminInitiateAuthAsync(string clientId, string userPoolId, string userName, string password) + { + var authParameters = new Dictionary(); + authParameters.Add("USERNAME", userName); + authParameters.Add("PASSWORD", password); + + var request = new AdminInitiateAuthRequest + { + ClientId = clientId, + UserPoolId = userPoolId, + AuthParameters = authParameters, + AuthFlow = AuthFlowType.ADMIN_USER_PASSWORD_AUTH, + }; + + var response = await _cognitoService.AdminInitiateAuthAsync(request); + return response.Session; + } + // snippet-end:[Cognito.dotnetv4.AdminInitiateAuth] + + // snippet-start:[Cognito.dotnetv4.InitiateAuth] + /// + /// Initiate authorization. + /// + /// The client Id of the application. + /// The name of the user who is authenticating. + /// The password for the user who is authenticating. + /// The response from the initiate auth request. + public async Task InitiateAuthAsync(string clientId, string userName, string password) + { + var authParameters = new Dictionary(); + authParameters.Add("USERNAME", userName); + authParameters.Add("PASSWORD", password); + + var authRequest = new InitiateAuthRequest + + { + ClientId = clientId, + AuthParameters = authParameters, + AuthFlow = AuthFlowType.USER_PASSWORD_AUTH, + }; + + var response = await _cognitoService.InitiateAuthAsync(authRequest); + Console.WriteLine($"Result Challenge is : {response.ChallengeName}"); + + return response; + } + // snippet-end:[Cognito.dotnetv4.InitiateAuth] + + // snippet-start:[Cognito.dotnetv4.ConfirmSignUp] + /// + /// Confirm that the user has signed up. + /// + /// The Id of this application. + /// The confirmation code sent to the user. + /// The username. + /// True if successful. + public async Task ConfirmSignupAsync(string clientId, string code, string userName) + { + var signUpRequest = new ConfirmSignUpRequest + { + ClientId = clientId, + ConfirmationCode = code, + Username = userName, + }; + + var response = await _cognitoService.ConfirmSignUpAsync(signUpRequest); + if (response.HttpStatusCode == HttpStatusCode.OK) + { + Console.WriteLine($"{userName} was confirmed"); + return true; + } + return false; + } + + // snippet-end:[Cognito.dotnetv4.ConfirmSignUp] + + // snippet-start:[Cognito.dotnetv4.ConfirmDevice] + /// + /// Initiates and confirms tracking of the device. + /// + /// The user's access token. + /// The key of the device from Amazon Cognito. + /// The device name. + /// + public async Task ConfirmDeviceAsync(string accessToken, string deviceKey, string deviceName) + { + var request = new ConfirmDeviceRequest + { + AccessToken = accessToken, + DeviceKey = deviceKey, + DeviceName = deviceName + }; + + var response = await _cognitoService.ConfirmDeviceAsync(request); + return response.UserConfirmationNecessary; + } + + // snippet-end:[Cognito.dotnetv4.ConfirmDevice] + + // snippet-start:[Cognito.dotnetv4.ResendConfirmationCode] + /// + /// Send a new confirmation code to a user. + /// + /// The Id of the client application. + /// The username of user who will receive the code. + /// The delivery details. + public async Task ResendConfirmationCodeAsync(string clientId, string userName) + { + var codeRequest = new ResendConfirmationCodeRequest + { + ClientId = clientId, + Username = userName, + }; + + var response = await _cognitoService.ResendConfirmationCodeAsync(codeRequest); + + Console.WriteLine($"Method of delivery is {response.CodeDeliveryDetails.DeliveryMedium}"); + + return response.CodeDeliveryDetails; + } + + // snippet-end:[Cognito.dotnetv4.ResendConfirmationCode] + + // snippet-start:[Cognito.dotnetv4.GetAdminUser] + /// + /// Get the specified user from an Amazon Cognito user pool with administrator access. + /// + /// The name of the user. + /// The Id of the Amazon Cognito user pool. + /// Async task. + public async Task GetAdminUserAsync(string userName, string poolId) + { + AdminGetUserRequest userRequest = new AdminGetUserRequest + { + Username = userName, + UserPoolId = poolId, + }; + + var response = await _cognitoService.AdminGetUserAsync(userRequest); + + Console.WriteLine($"User status {response.UserStatus}"); + return response.UserStatus; + } + + // snippet-end:[Cognito.dotnetv4.GetAdminUser] + + // snippet-start:[Cognito.dotnetv4.SignUp] + /// + /// Sign up a new user. + /// + /// The client Id of the application. + /// The username to use. + /// The user's password. + /// The email address of the user. + /// A Boolean value indicating whether the user was confirmed. + public async Task SignUpAsync(string clientId, string userName, string password, string email) + { + var userAttrs = new AttributeType + { + Name = "email", + Value = email, + }; + + var userAttrsList = new List(); + + userAttrsList.Add(userAttrs); + + var signUpRequest = new SignUpRequest + { + UserAttributes = userAttrsList, + Username = userName, + ClientId = clientId, + Password = password + }; + + var response = await _cognitoService.SignUpAsync(signUpRequest); + return response.HttpStatusCode == HttpStatusCode.OK; + } + + // snippet-end:[Cognito.dotnetv4.SignUp] +} + +// snippet-end:[Cognito.dotnetv4.CognitoWrapper] \ No newline at end of file diff --git a/dotnetv4/Cognito/Actions/HelloCognito.cs b/dotnetv4/Cognito/Actions/HelloCognito.cs new file mode 100644 index 00000000000..230a4d86799 --- /dev/null +++ b/dotnetv4/Cognito/Actions/HelloCognito.cs @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Cognito.dotnetv4.HelloCognito] + +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace CognitoActions; + +/// +/// A class that introduces the Amazon Cognito Identity Provider by listing the +/// user pools for the account. +/// +public class HelloCognito +{ + private static ILogger logger = null!; + + static async Task Main(string[] args) + { + // Set up dependency injection for Amazon Cognito. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + logging.AddFilter("System", LogLevel.Debug) + .AddFilter("Microsoft", LogLevel.Information) + .AddFilter("Microsoft", LogLevel.Trace)) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddTransient() + ) + .Build(); + + logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) + .CreateLogger(); + + var amazonClient = host.Services.GetRequiredService(); + + Console.Clear(); + Console.WriteLine("Hello Amazon Cognito."); + Console.WriteLine("Let's get a list of your Amazon Cognito user pools."); + + var userPools = new List(); + + var userPoolsPaginator = amazonClient.Paginators.ListUserPools(new ListUserPoolsRequest()); + + await foreach (var response in userPoolsPaginator.Responses) + { + userPools.AddRange(response.UserPools); + } + + if (userPools.Count > 0) + { + userPools.ForEach(userPool => + { + Console.WriteLine($"{userPool.Name}\t{userPool.Id}"); + }); + } + else + { + Console.WriteLine("No user pools were found."); + } + } +} + +// snippet-end:[Cognito.dotnetv4.HelloCognito] \ No newline at end of file diff --git a/dotnetv4/Cognito/Actions/Usings.cs b/dotnetv4/Cognito/Actions/Usings.cs new file mode 100644 index 00000000000..5b7cea27136 --- /dev/null +++ b/dotnetv4/Cognito/Actions/Usings.cs @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Cognito.dotnetv3.Usings] +global using Amazon.CognitoIdentityProvider; +global using Amazon.CognitoIdentityProvider.Model; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Console; +global using Microsoft.Extensions.Logging.Debug; + +// snippet-end:[Cognito.dotnetv3.Usings] \ No newline at end of file diff --git a/dotnetv4/Cognito/CognitoExamples.sln b/dotnetv4/Cognito/CognitoExamples.sln new file mode 100644 index 00000000000..694f56abe02 --- /dev/null +++ b/dotnetv4/Cognito/CognitoExamples.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{7907FB6A-1353-4735-95DC-EEC5DF8C0649}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{B987097B-189C-4D0B-99BC-E67CD705BCA0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5455D423-2AFC-4BC6-B79D-9DC4270D8F7D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CognitoActions", "Actions\CognitoActions.csproj", "{796910FA-6E94-460B-8CB4-97DF01B9ADC8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CognitoBasics", "Scenarios\Cognito_Basics\CognitoBasics.csproj", "{B1731AE1-381F-4044-BEBE-269FF7E24B1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CognitoTests", "Tests\CognitoTests.csproj", "{6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.Build.0 = Release|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.Build.0 = Release|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {796910FA-6E94-460B-8CB4-97DF01B9ADC8} = {7907FB6A-1353-4735-95DC-EEC5DF8C0649} + {B1731AE1-381F-4044-BEBE-269FF7E24B1F} = {B987097B-189C-4D0B-99BC-E67CD705BCA0} + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88} = {5455D423-2AFC-4BC6-B79D-9DC4270D8F7D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {870D888D-5C8B-4057-8722-F73ECF38E513} + EndGlobalSection +EndGlobal diff --git a/dotnetv4/Cognito/README.md b/dotnetv4/Cognito/README.md new file mode 100644 index 00000000000..677b1901dae --- /dev/null +++ b/dotnetv4/Cognito/README.md @@ -0,0 +1,138 @@ +# Amazon Cognito Identity Provider code examples for the SDK for .NET + +## Overview + +Shows how to use the AWS SDK for .NET to work with Amazon Cognito Identity Provider. + + + + +_Amazon Cognito Identity Provider handles user authentication and authorization for your web and mobile apps._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + + + +These examples also require the following resources: + +* An existing Amazon Cognito user pool that is configured to allow self sign-up. +* A client ID to use for authenticating with Amazon Cognito. + + +To create these resources, run the AWS CloudFormation script in the +[resources/cdk/cognito_scenario_user_pool_with_mfa](../../resources/cdk/cognito_scenario_user_pool_with_mfa) +folder. This script outputs a user pool ID and a client ID that you can use to run +the scenario. + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [AdminGetUser](Actions/CognitoWrapper.cs#L288) +- [AdminInitiateAuth](Actions/CognitoWrapper.cs#L156) +- [AdminRespondToAuthChallenge](Actions/CognitoWrapper.cs#L72) +- [AssociateSoftwareToken](Actions/CognitoWrapper.cs#L133) +- [ConfirmDevice](Actions/CognitoWrapper.cs#L241) +- [ConfirmSignUp](Actions/CognitoWrapper.cs#L213) +- [InitiateAuth](Actions/CognitoWrapper.cs#L184) +- [ListUserPools](Actions/CognitoWrapper.cs#L25) +- [ListUsers](Actions/CognitoWrapper.cs#L46) +- [ResendConfirmationCode](Actions/CognitoWrapper.cs#L264) +- [SignUp](Actions/CognitoWrapper.cs#L311) +- [VerifySoftwareToken](Actions/CognitoWrapper.cs#L111) + +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Sign up a user with a user pool that requires MFA](Actions/CognitoWrapper.cs) + + + + + +## Run the examples + +### Instructions + +For general instructions to run the examples, see the +[README](../README.md#building-and-running-the-code-examples) in the `dotnetv4` folder. + +Some projects might include a settings.json file. Before compiling the project, +you can change these values to match your own account and resources. Alternatively, +add a settings.local.json file with your local settings, which will be loaded automatically +when the application runs. + +After the example compiles, you can run it from the command line. To do so, navigate to +the folder that contains the .csproj file and run the following command: + +``` +dotnet run +``` + +Alternatively, you can run the example from within your IDE. + + + + + + + +#### Sign up a user with a user pool that requires MFA + +This example shows you how to do the following: + +- Sign up and confirm a user with a username, password, and email address. +- Set up multi-factor authentication by associating an MFA application with the user. +- Sign in by using a password and an MFA code. + + + + + + + + +### 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 `dotnetv4` folder. + + + + + + +## Additional resources + +- [Amazon Cognito Identity Provider Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) +- [Amazon Cognito Identity Provider API Reference](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/Welcome.html) +- [SDK for .NET Amazon Cognito Identity Provider reference](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/CognitoIdentity/NCognitoIdentity.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.cs b/dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.cs new file mode 100644 index 00000000000..a5418365f5f --- /dev/null +++ b/dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.cs @@ -0,0 +1,160 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Cognito.dotnetv4.Main] + +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace CognitoBasics; + +public static class CognitoBasics +{ + public static bool _interactive = true; + + public static async Task Main(string[] args) + { + // Set up dependency injection for Amazon Cognito. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + logging.AddFilter("System", LogLevel.Debug) + .AddFilter("Microsoft", LogLevel.Information) + .AddFilter("Microsoft", LogLevel.Trace)) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddTransient() + ) + .Build(); ; + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("settings.json") // Load settings from .json file. + .AddJsonFile("settings.local.json", + true) // Optionally load local settings. + .Build(); + + var cognitoWrapper = host.Services.GetRequiredService(); + + await RunScenario(cognitoWrapper, configuration); + } + + /// + /// Run the example scenario. + /// + /// Wrapper for service actions. + /// Scenario configuration. + /// + public static async Task RunScenario(CognitoWrapper cognitoWrapper, IConfigurationRoot configuration) + { + Console.WriteLine(new string('-', 80)); + UiMethods.DisplayOverview(); + Console.WriteLine(new string('-', 80)); + + // clientId - The app client Id value that you get from the AWS CDK script. + var clientId = + configuration[ + "ClientId"]; // "*** REPLACE WITH CLIENT ID VALUE FROM CDK SCRIPT"; + + // poolId - The pool Id that you get from the AWS CDK script. + var poolId = + configuration["PoolId"]!; // "*** REPLACE WITH POOL ID VALUE FROM CDK SCRIPT"; + var userName = configuration["UserName"]; + var password = configuration["Password"]; + var email = configuration["Email"]; + + // If the username wasn't set in the configuration file, + // get it from the user now. + if (userName is null) + { + do + { + Console.Write("Username: "); + userName = Console.ReadLine(); + } while (string.IsNullOrEmpty(userName)); + } + + Console.WriteLine($"\nUsername: {userName}"); + + // If the password wasn't set in the configuration file, + // get it from the user now. + if (password is null) + { + do + { + Console.Write("Password: "); + password = Console.ReadLine(); + } while (string.IsNullOrEmpty(password)); + } + + // If the email address wasn't set in the configuration file, + // get it from the user now. + if (email is null) + { + do + { + Console.Write("Email: "); + email = Console.ReadLine(); + } while (string.IsNullOrEmpty(email)); + } + + // Now sign up the user. + Console.WriteLine($"\nSigning up {userName} with email address: {email}"); + await cognitoWrapper.SignUpAsync(clientId, userName, password, email); + + // Add the user to the user pool. + Console.WriteLine($"Adding {userName} to the user pool"); + await cognitoWrapper.GetAdminUserAsync(userName, poolId); + + UiMethods.DisplayTitle("Get confirmation code"); + Console.WriteLine($"Conformation code sent to {userName}."); + + Console.Write("Would you like to send a new code? (Y/N) "); + var answer = _interactive ? Console.ReadLine() : "y"; + + if (answer!.ToLower() == "y") + { + await cognitoWrapper.ResendConfirmationCodeAsync(clientId, userName); + Console.WriteLine("Sending a new confirmation code"); + } + + Console.Write("Enter confirmation code (from Email): "); + var code = _interactive ? Console.ReadLine() : "-"; + + await cognitoWrapper.ConfirmSignupAsync(clientId, code, userName); + + + UiMethods.DisplayTitle("Checking status"); + Console.WriteLine($"Rechecking the status of {userName} in the user pool"); + await cognitoWrapper.GetAdminUserAsync(userName, poolId); + + Console.WriteLine($"Setting up authenticator for {userName} in the user pool"); + var setupResponse = await cognitoWrapper.InitiateAuthAsync(clientId, userName, password); + + var setupSession = await cognitoWrapper.AssociateSoftwareTokenAsync(setupResponse.Session); + Console.Write("Enter the 6-digit code displayed in Google Authenticator: "); + var setupCode = _interactive ? Console.ReadLine() : "-"; + var setupResult = + await cognitoWrapper.VerifySoftwareTokenAsync(setupSession, setupCode); + Console.WriteLine($"Setup status: {setupResult}"); + + Console.WriteLine($"Now logging in {userName} in the user pool"); + var authSession = + await cognitoWrapper.AdminInitiateAuthAsync(clientId, poolId, userName, + password); + + Console.Write("Enter a new 6-digit code displayed in Google Authenticator: "); + var authCode = _interactive ? Console.ReadLine() : "-"; + var authResult = + await cognitoWrapper.AdminRespondToAuthChallengeAsync(userName, clientId, + authCode, authSession, poolId); + Console.WriteLine( + $"Authenticated and received access token: {authResult.AccessToken}"); + + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Cognito scenario is complete."); + Console.WriteLine(new string('-', 80)); + return true; + } +} + +// snippet-end:[Cognito.dotnetv4.Main] \ No newline at end of file diff --git a/dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.csproj b/dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.csproj new file mode 100644 index 00000000000..fdf7a548655 --- /dev/null +++ b/dotnetv4/Cognito/Scenarios/Cognito_Basics/CognitoBasics.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + settings.json + + + + diff --git a/dotnetv4/Cognito/Scenarios/Cognito_Basics/UIMethods.cs b/dotnetv4/Cognito/Scenarios/Cognito_Basics/UIMethods.cs new file mode 100644 index 00000000000..ccc9c967e24 --- /dev/null +++ b/dotnetv4/Cognito/Scenarios/Cognito_Basics/UIMethods.cs @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Cognito.dotnetv4.UIMethods] +namespace CognitoBasics; + +/// +/// Some useful methods to make screen display easier. +/// +public static class UiMethods +{ + /// + /// Show information about the scenario. + /// + public static void DisplayOverview() + { + DisplayTitle("Welcome to the Amazon Cognito Demo"); + + Console.WriteLine("This example application does the following:"); + Console.WriteLine("\t 1. Signs up a user."); + Console.WriteLine("\t 2. Gets the user's confirmation status."); + Console.WriteLine("\t 3. Resends the confirmation code if the user requested another code."); + Console.WriteLine("\t 4. Confirms that the user signed up."); + Console.WriteLine("\t 5. Invokes the initiateAuth to sign in. This results in being prompted to set up TOTP (time-based one-time password). (The response is “ChallengeName”: “MFA_SETUP”)."); + Console.WriteLine("\t 6. Invokes the AssociateSoftwareToken method to generate a TOTP MFA private key. This can be used with Google Authenticator."); + Console.WriteLine("\t 7. Invokes the VerifySoftwareToken method to verify the TOTP and register for MFA."); + Console.WriteLine("\t 8. Invokes the AdminInitiateAuth to sign in again. This results in being prompted to submit a TOTP (Response: “ChallengeName”: “SOFTWARE_TOKEN_MFA”)."); + Console.WriteLine("\t 9. Invokes the AdminRespondToAuthChallenge to get back a token."); + } + + /// + /// Display a line of hyphens, the centered text of the title and another + /// line of hyphens. + /// + /// The string to be displayed. + public static void DisplayTitle(string strTitle) + { + Console.WriteLine(); + Console.WriteLine(strTitle); + Console.WriteLine(); + } +} + +// snippet-end:[Cognito.dotnetv4.UIMethods] \ No newline at end of file diff --git a/dotnetv4/Cognito/Scenarios/Cognito_Basics/Usings.cs b/dotnetv4/Cognito/Scenarios/Cognito_Basics/Usings.cs new file mode 100644 index 00000000000..8a06b87643b --- /dev/null +++ b/dotnetv4/Cognito/Scenarios/Cognito_Basics/Usings.cs @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Cognito.dotnetv4.CognitoBasics.Usings] +global using Amazon.CognitoIdentityProvider; +global using CognitoActions; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Console; +global using Microsoft.Extensions.Logging.Debug; + +// snippet-end:[Cognito.dotnetv4.CognitoBasics.Usings] \ No newline at end of file diff --git a/dotnetv4/Cognito/Scenarios/Cognito_Basics/settings.json b/dotnetv4/Cognito/Scenarios/Cognito_Basics/settings.json new file mode 100644 index 00000000000..4bfac53daa4 --- /dev/null +++ b/dotnetv4/Cognito/Scenarios/Cognito_Basics/settings.json @@ -0,0 +1,9 @@ +{ + "ClientId": "client_id_from_cdk", + "PoolId": "client_id_from_cdk", + "UserName": "username", + "Password": "EXAMPLEPASSWORD", + "Email": "useremail", + "adminUserName": "admin", + "adminPassword": "EXAMPLEPASSWORD" +} diff --git a/dotnetv4/Cognito/Tests/CognitoBasicsTests.cs b/dotnetv4/Cognito/Tests/CognitoBasicsTests.cs new file mode 100644 index 00000000000..974973c7b8f --- /dev/null +++ b/dotnetv4/Cognito/Tests/CognitoBasicsTests.cs @@ -0,0 +1,198 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Net; +using Amazon.CognitoIdentityProvider; +using Amazon.CognitoIdentityProvider.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; + +namespace CognitoWrapperTests; + +/// +/// Tests for the Cognito scenario. +/// +public class CognitoBasicsTests +{ + private ILoggerFactory _loggerFactory = null!; + + [Trait("Category", "Unit")] + [Fact] + public async Task ScenarioTest() + { + // Arrange. + _loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + + var mockCognitoService = new Mock(); + + mockCognitoService.Setup(client => client.Paginators.ListUserPools( + It.IsAny())) + .Returns(new TestUserPoolPaginator() as IListUserPoolsPaginator); + + mockCognitoService.Setup(client => client.Paginators.ListUserPools( + It.IsAny())) + .Returns(new TestUserPoolPaginator() as IListUserPoolsPaginator); + + mockCognitoService.Setup(client => client.AdminRespondToAuthChallengeAsync( + It.IsAny(), + It.IsAny())) + .Returns((AdminRespondToAuthChallengeRequest r, + CancellationToken token) => + { + return Task.FromResult(new AdminRespondToAuthChallengeResponse() + { + HttpStatusCode = HttpStatusCode.OK, + AuthenticationResult = new AuthenticationResultType() + }); + }); + + mockCognitoService.Setup(client => client.VerifySoftwareTokenAsync( + It.IsAny(), + It.IsAny())) + .Returns((VerifySoftwareTokenRequest r, + CancellationToken token) => + { + return Task.FromResult(new VerifySoftwareTokenResponse() + { + HttpStatusCode = HttpStatusCode.OK, + }); + }); + + mockCognitoService.Setup(client => client.AssociateSoftwareTokenAsync( + It.IsAny(), + It.IsAny())) + .Returns((AssociateSoftwareTokenRequest r, + CancellationToken token) => + { + return Task.FromResult(new AssociateSoftwareTokenResponse() + { + HttpStatusCode = HttpStatusCode.OK, + }); + }); + + mockCognitoService.Setup(client => client.AdminInitiateAuthAsync( + It.IsAny(), + It.IsAny())) + .Returns((AdminInitiateAuthRequest r, + CancellationToken token) => + { + return Task.FromResult(new AdminInitiateAuthResponse() + { + HttpStatusCode = HttpStatusCode.OK, + }); + }); + + mockCognitoService.Setup(client => client.InitiateAuthAsync( + It.IsAny(), + It.IsAny())) + .Returns((InitiateAuthRequest r, + CancellationToken token) => + { + return Task.FromResult(new InitiateAuthResponse() + { + HttpStatusCode = HttpStatusCode.OK, + }); + }); + + mockCognitoService.Setup(client => client.ConfirmSignUpAsync( + It.IsAny(), + It.IsAny())) + .Returns((ConfirmSignUpRequest r, + CancellationToken token) => + { + return Task.FromResult(new ConfirmSignUpResponse() + { + HttpStatusCode = HttpStatusCode.OK, + }); + }); + + mockCognitoService.Setup(client => client.ResendConfirmationCodeAsync( + It.IsAny(), + It.IsAny())) + .Returns((ResendConfirmationCodeRequest r, + CancellationToken token) => + { + return Task.FromResult(new ResendConfirmationCodeResponse() + { + HttpStatusCode = HttpStatusCode.OK, + CodeDeliveryDetails = new CodeDeliveryDetailsType() + }); + }); + + mockCognitoService.Setup(client => client.AdminGetUserAsync( + It.IsAny(), + It.IsAny())) + .Returns((AdminGetUserRequest r, + CancellationToken token) => + { + return Task.FromResult(new AdminGetUserResponse() + { + HttpStatusCode = HttpStatusCode.OK, + UserStatus = UserStatusType.CONFIRMED + }); + }); + + mockCognitoService.Setup(client => client.SignUpAsync( + It.IsAny(), + It.IsAny())) + .Returns((SignUpRequest r, + CancellationToken token) => + { + return Task.FromResult(new SignUpResponse() + { + HttpStatusCode = HttpStatusCode.OK, + }); + }); + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("testsettings.json") // Load test settings from .json file. + .AddJsonFile("testsettings.local.json", + true) // Optionally load local settings. + .Build(); + + var wrapper = new CognitoWrapper(mockCognitoService.Object); + CognitoBasics.CognitoBasics._interactive = false; + + var success = + await CognitoBasics.CognitoBasics.RunScenario(wrapper, configuration); + Assert.True(success); + } + +} + + +/// +/// Mock Paginator for user pool response. +/// +public class TestUsersPaginator : IPaginator, IListUsersPaginator +{ + public IAsyncEnumerable PaginateAsync( + CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public IPaginatedEnumerable Responses { get; } = null!; + public IPaginatedEnumerable Users { get; } = null!; +} + +/// +/// Mock Paginator for user response. +/// +public class TestUserPoolPaginator : IPaginator, IListUserPoolsPaginator +{ + public IAsyncEnumerable PaginateAsync( + CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public IPaginatedEnumerable Responses { get; } = null!; + public IPaginatedEnumerable UserPools { get; } = null!; +} \ No newline at end of file diff --git a/dotnetv4/Cognito/Tests/CognitoTests.csproj b/dotnetv4/Cognito/Tests/CognitoTests.csproj new file mode 100644 index 00000000000..fb9883ad93d --- /dev/null +++ b/dotnetv4/Cognito/Tests/CognitoTests.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + PreserveNewest + + + PreserveNewest + testsettings.json + + + + + + + + + diff --git a/dotnetv4/Cognito/Tests/Usings.cs b/dotnetv4/Cognito/Tests/Usings.cs new file mode 100644 index 00000000000..d77a2d566c5 --- /dev/null +++ b/dotnetv4/Cognito/Tests/Usings.cs @@ -0,0 +1,8 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using CognitoActions; +global using Xunit; + +// Optional. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/dotnetv4/Cognito/Tests/testsettings.json b/dotnetv4/Cognito/Tests/testsettings.json new file mode 100644 index 00000000000..eefdb2c8435 --- /dev/null +++ b/dotnetv4/Cognito/Tests/testsettings.json @@ -0,0 +1,8 @@ +{ + "UserName": "someuser", + "Email": "someone@example.com", + "Password": "AGoodPassword1234", + "UserPoolId": "IDENTIFY_POOL_ID", + "ClientId": "CLIENT_ID_FROM_CDK_SCRIPT", + "PoolId": "USER_POOL_ID_FROM_CDK_SCRIPT" +} diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln index ab7be69d4d9..d46afcd8c1e 100644 --- a/dotnetv4/DotNetV4Examples.sln +++ b/dotnetv4/DotNetV4Examples.sln @@ -119,6 +119,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "EC2\Scenarios\EC2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EC2Actions", "EC2\Actions\EC2Actions.csproj", "{0633CB2B-3508-48E5-A8C2-427A83A5CA6E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cognito", "Cognito", "{F5214562-85F4-4FD8-B56D-C5D8E7914901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitoTests", "Cognito\Tests\CognitoTests.csproj", "{63DC05A0-5B16-45A4-BDE5-90DD2E200507}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{D38A409C-EE40-4E70-B500-F3D6EF8E82A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitoBasics", "Cognito\Scenarios\Cognito_Basics\CognitoBasics.csproj", "{38C8C3B0-163D-4B7B-86A2-3EFFBC165E99}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitoActions", "Cognito\Actions\CognitoActions.csproj", "{1AF980DF-DEEA-4E5D-9001-6EC67EB96AD1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -293,6 +303,18 @@ Global {0633CB2B-3508-48E5-A8C2-427A83A5CA6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {0633CB2B-3508-48E5-A8C2-427A83A5CA6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {0633CB2B-3508-48E5-A8C2-427A83A5CA6E}.Release|Any CPU.Build.0 = Release|Any CPU + {63DC05A0-5B16-45A4-BDE5-90DD2E200507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63DC05A0-5B16-45A4-BDE5-90DD2E200507}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63DC05A0-5B16-45A4-BDE5-90DD2E200507}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63DC05A0-5B16-45A4-BDE5-90DD2E200507}.Release|Any CPU.Build.0 = Release|Any CPU + {38C8C3B0-163D-4B7B-86A2-3EFFBC165E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38C8C3B0-163D-4B7B-86A2-3EFFBC165E99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38C8C3B0-163D-4B7B-86A2-3EFFBC165E99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38C8C3B0-163D-4B7B-86A2-3EFFBC165E99}.Release|Any CPU.Build.0 = Release|Any CPU + {1AF980DF-DEEA-4E5D-9001-6EC67EB96AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AF980DF-DEEA-4E5D-9001-6EC67EB96AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AF980DF-DEEA-4E5D-9001-6EC67EB96AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AF980DF-DEEA-4E5D-9001-6EC67EB96AD1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -349,6 +371,10 @@ Global {6C167F25-F97F-4854-8CD8-A2D446B6799B} = {9424FB14-B6DE-44CE-B675-AC2B57EC1E69} {D95519CA-BD27-45AE-B83B-3FB02E7AE445} = {6C167F25-F97F-4854-8CD8-A2D446B6799B} {0633CB2B-3508-48E5-A8C2-427A83A5CA6E} = {9424FB14-B6DE-44CE-B675-AC2B57EC1E69} + {63DC05A0-5B16-45A4-BDE5-90DD2E200507} = {F5214562-85F4-4FD8-B56D-C5D8E7914901} + {D38A409C-EE40-4E70-B500-F3D6EF8E82A4} = {F5214562-85F4-4FD8-B56D-C5D8E7914901} + {38C8C3B0-163D-4B7B-86A2-3EFFBC165E99} = {D38A409C-EE40-4E70-B500-F3D6EF8E82A4} + {1AF980DF-DEEA-4E5D-9001-6EC67EB96AD1} = {F5214562-85F4-4FD8-B56D-C5D8E7914901} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} From 4ab44292024c8329419e1c4402bf3ebc1cf4f7de Mon Sep 17 00:00:00 2001 From: Dennis Traub Date: Tue, 11 Feb 2025 15:59:55 +0100 Subject: [PATCH 044/144] Fix metadata for Go v2 Bedrock Runtime example (#7240) --- .doc_gen/metadata/bedrock-runtime_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index 5eeddc97e7f..8d11bff779c 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -918,7 +918,7 @@ bedrock-runtime_InvokeModelWithResponseStream_AnthropicClaude: excerpts: - description: Use the Invoke Model API to send a text message and process the response stream in real-time. snippet_tags: - - gov2.bedrock-runtime.InvokeModelWrapper.struct + - gov2.bedrock-runtime.InvokeModelWithResponseStreamWrapper.struct - gov2.bedrock-runtime.InvokeModelWithResponseStream JavaScript: versions: From f38d2e858a45595e19924cb5b05f52f6fdd54aef Mon Sep 17 00:00:00 2001 From: ford prior Date: Tue, 11 Feb 2025 10:34:00 -0500 Subject: [PATCH 045/144] Tools - Weathertop: Update targets.yaml (#7235) --- .tools/test/stacks/config/targets.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.tools/test/stacks/config/targets.yaml b/.tools/test/stacks/config/targets.yaml index 7686219e298..974a05fa81f 100644 --- a/.tools/test/stacks/config/targets.yaml +++ b/.tools/test/stacks/config/targets.yaml @@ -13,10 +13,10 @@ javascriptv3: account_id: "875008041426" status: "enabled" javav2: - account_id: "667348412466" # back-up "814548047983" + account_id: "814548047983" # back-up "667348412466" status: "enabled" kotlin: - account_id: "471951630130" # back-up "814548047983" + account_id: "814548047983" # back-up "471951630130" status: "enabled" php: account_id: "733931915187" From d8733a8b50d0b15b4a1defd9d1a68d3e8869fab4 Mon Sep 17 00:00:00 2001 From: "Brandon C (Amazon)" Date: Wed, 12 Feb 2025 08:01:49 -0800 Subject: [PATCH 046/144] JavaScript: Fixed syntax for Lambda trigger event handler : Amazon Cognito (#7220) --- .../cognito/lambda-trigger-pre-sign-up-auto-confirm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/example_code/cognito/lambda-trigger-pre-sign-up-auto-confirm.js b/javascript/example_code/cognito/lambda-trigger-pre-sign-up-auto-confirm.js index 0e6881e3856..7c04c357d7f 100644 --- a/javascript/example_code/cognito/lambda-trigger-pre-sign-up-auto-confirm.js +++ b/javascript/example_code/cognito/lambda-trigger-pre-sign-up-auto-confirm.js @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // snippet-start:[cognito.javascript.lambda-trigger.pre-sign-up-auto-confirm] -exports.handler = (event, context, callback) => { +export const handler = async (event, context, callback) => { // Set the user pool autoConfirmUser flag after validating the email domain event.response.autoConfirmUser = false; From aa639887accecaae642928a456c0ed4ddc1699c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:11:17 -0500 Subject: [PATCH 047/144] Bump aws-cdk-lib from 2.82.0 to 2.177.0 in /applications/photo-asset-manager/cdk (#7231) Bump aws-cdk-lib in /applications/photo-asset-manager/cdk Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk-lib) from 2.82.0 to 2.177.0. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.177.0/packages/aws-cdk-lib) --- updated-dependencies: - dependency-name: aws-cdk-lib dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../photo-asset-manager/cdk/package-lock.json | 145 +++++++++++------- .../photo-asset-manager/cdk/package.json | 2 +- 2 files changed, 92 insertions(+), 55 deletions(-) diff --git a/applications/photo-asset-manager/cdk/package-lock.json b/applications/photo-asset-manager/cdk/package-lock.json index bb25e68274e..3effaf325bf 100644 --- a/applications/photo-asset-manager/cdk/package-lock.json +++ b/applications/photo-asset-manager/cdk/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@aws-cdk/aws-cloudformation": "^1.196.0", "@aws-sdk/client-cloudformation": "^3.621.0", - "aws-cdk-lib": "^2.82.0", + "aws-cdk-lib": "^2.177.0", "constructs": "^10.0.0" }, "bin": { @@ -24,19 +24,22 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.185", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.185.tgz", - "integrity": "sha512-cost0pu5nsmQmFhVxN4OonThGhgQeSlwntdXsEi5v8buVg+X4MzcXemmmSZxkkzzFCoS0r4w/7BiX1e+mMkFVA==" + "version": "2.2.222", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.222.tgz", + "integrity": "sha512-9qjd91FwBYmxjfF3ckieTKrmmvIBZdSe1Daf/hRGxAPnhtH9Fm5Y3Oi0dJD2tRw0ufyM6AbvX9zgejcTqXc+LQ==", + "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-kubectl-v20": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.1.tgz", - "integrity": "sha512-U1ntiX8XiMRRRH5J1IdC+1t5CE89015cwyt5U63Cpk0GnMlN5+h9WsWMlKlPXZR4rdq/m806JRlBMRpBUB2Dhw==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", + "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==", + "license": "Apache-2.0" }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v5": { - "version": "2.0.155", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.155.tgz", - "integrity": "sha512-Q+Ny25hUPINlBbS6lmbUr4m6Tr6ToEJBla7sXA3FO3JUD0Z69ddcgbhuEBF8Rh1a2xmPONm89eX77kwK2fb4vQ==" + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" }, "node_modules/@aws-cdk/assets": { "version": "1.196.0", @@ -2232,9 +2235,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.82.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.82.0.tgz", - "integrity": "sha512-icLhHvoxxo5mu9z8oplSHF+A7scbRiXYoRp2hyFkYSCoY9H+eBeIVXKA2S5YPpJfJO4SeORbCQnsyXBbz31XXw==", + "version": "2.177.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.177.0.tgz", + "integrity": "sha512-nTnHAwjZaPJ5gfJjtzE/MyK6q0a66nWthoJl7l8srucRb+I30dczhbbXor6QCdVpJaTRAEliMOMq23aglsAQbg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2245,21 +2248,25 @@ "punycode", "semver", "table", - "yaml" + "yaml", + "mime-types" ], + "license": "Apache-2.0", "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.177", - "@aws-cdk/asset-kubectl-v20": "^2.1.1", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-kubectl-v20": "^2.1.3", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^39.2.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.1.1", - "ignore": "^5.2.4", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", "minimatch": "^3.1.2", - "punycode": "^2.3.0", - "semver": "^7.5.1", - "table": "^6.8.1", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", "yaml": "1.10.2" }, "engines": { @@ -2269,20 +2276,53 @@ "constructs": "^10.0.0" } }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "39.2.15", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.15.tgz", + "integrity": "sha512-roeUKO5QR9JLnNEULg0RiS1ac6PZ9qsPaOcAJXCP0D1NLLECdxwwqJvLbhV91pCWrGTeWY5OhLtlL5OPS6Ycvg==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { "version": "1.0.2", "inBundle": true, "license": "Apache-2.0" }, "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.12.0", + "version": "8.17.1", "inBundle": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2372,8 +2412,13 @@ "inBundle": true, "license": "MIT" }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.1.1", + "version": "11.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -2391,7 +2436,7 @@ "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.2.4", + "version": "5.3.2", "inBundle": true, "license": "MIT", "engines": { @@ -2435,15 +2480,23 @@ "inBundle": true, "license": "MIT" }, - "node_modules/aws-cdk-lib/node_modules/lru-cache": { - "version": "6.0.0", + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", "inBundle": true, - "license": "ISC", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, "node_modules/aws-cdk-lib/node_modules/minimatch": { @@ -2458,7 +2511,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.0", + "version": "2.3.1", "inBundle": true, "license": "MIT", "engines": { @@ -2474,12 +2527,9 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.5.1", + "version": "7.6.3", "inBundle": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -2528,7 +2578,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.1", + "version": "6.8.2", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -2543,26 +2593,13 @@ } }, "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", "inBundle": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/uri-js": { - "version": "4.4.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, "node_modules/aws-cdk-lib/node_modules/yaml": { "version": "1.10.2", "inBundle": true, diff --git a/applications/photo-asset-manager/cdk/package.json b/applications/photo-asset-manager/cdk/package.json index c0903aacfd2..6d44900e986 100644 --- a/applications/photo-asset-manager/cdk/package.json +++ b/applications/photo-asset-manager/cdk/package.json @@ -18,7 +18,7 @@ "dependencies": { "@aws-cdk/aws-cloudformation": "^1.196.0", "@aws-sdk/client-cloudformation": "^3.621.0", - "aws-cdk-lib": "^2.82.0", + "aws-cdk-lib": "^2.177.0", "constructs": "^10.0.0" } } From c2af65e1338fa3eed412d1c75c5925e1f54aa67f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:16:05 -0500 Subject: [PATCH 048/144] Bump certifi from 2024.6.2 to 2024.7.4 in /python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo (#7241) Bump certifi Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.6.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.06.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../cross-model-scenarios/tool_use_demo/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo/requirements.txt b/python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo/requirements.txt index e9bf5c64b29..6d21f66572b 100644 --- a/python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo/requirements.txt +++ b/python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo/requirements.txt @@ -1,6 +1,6 @@ boto3==1.34.124 botocore==1.34.124 -certifi==2024.6.2 +certifi==2024.7.4 charset-normalizer==3.3.2 idna==3.7 jmespath==1.0.1 From 619d99052934a3cfc2b1d2af225e1c65e84a56b0 Mon Sep 17 00:00:00 2001 From: Nicholas Doropoulos <13554823+nick22d@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:06:30 +0100 Subject: [PATCH 049/144] Feature/add update access key operation (#7211) --- .doc_gen/metadata/iam_metadata.yaml | 9 ++ aws-cli/bash-linux/iam/README.md | 7 +- .../iam_create_user_assume_role_scenario.sh | 9 ++ aws-cli/bash-linux/iam/iam_operations.sh | 117 ++++++++++++++++++ 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/.doc_gen/metadata/iam_metadata.yaml b/.doc_gen/metadata/iam_metadata.yaml index 383f52d39ff..480b9d7ee9f 100644 --- a/.doc_gen/metadata/iam_metadata.yaml +++ b/.doc_gen/metadata/iam_metadata.yaml @@ -889,6 +889,15 @@ iam_UpdateAccessKey: - description: snippet_tags: - iam.cpp.update_access_key.code + Bash: + versions: + - sdk_version: 2 + github: aws-cli/bash-linux/iam + sdkguide: + excerpts: + - description: + snippet_tags: + - aws-cli.bash-linux.iam.UpdateAccessKey services: iam: {UpdateAccessKey} iam_Scenario_ManageAccessKeys: diff --git a/aws-cli/bash-linux/iam/README.md b/aws-cli/bash-linux/iam/README.md index 19baf85864c..a204208e13e 100644 --- a/aws-cli/bash-linux/iam/README.md +++ b/aws-cli/bash-linux/iam/README.md @@ -45,14 +45,15 @@ Code excerpts that show you how to call individual service functions. - [CreatePolicy](iam_operations.sh#L421) - [CreateRole](iam_operations.sh#L342) - [CreateUser](iam_operations.sh#L113) -- [DeleteAccessKey](iam_operations.sh#L787) +- [DeleteAccessKey](iam_operations.sh#L904) - [DeletePolicy](iam_operations.sh#L646) - [DeleteRole](iam_operations.sh#L716) -- [DeleteUser](iam_operations.sh#L868) +- [DeleteUser](iam_operations.sh#L985) - [DetachRolePolicy](iam_operations.sh#L571) - [GetUser](iam_operations.sh#L17) - [ListAccessKeys](iam_operations.sh#L273) - [ListUsers](iam_operations.sh#L56) +- [UpdateAccessKey](iam_operations.sh#L787) @@ -110,4 +111,4 @@ in the `aws-cli` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/aws-cli/bash-linux/iam/iam_create_user_assume_role_scenario.sh b/aws-cli/bash-linux/iam/iam_create_user_assume_role_scenario.sh index d630c862708..2cb96f9d3fd 100755 --- a/aws-cli/bash-linux/iam/iam_create_user_assume_role_scenario.sh +++ b/aws-cli/bash-linux/iam/iam_create_user_assume_role_scenario.sh @@ -351,6 +351,15 @@ function clean_up() { fi fi + if [ -n "$access_key_name" ]; then + if (iam_update_access_key -u "$user_name" -k "$access_key_name" -d); then + echo "Deactivated access key $access_key_name" + else + errecho "The access key failed to deactivate." + result=1 + fi + fi + if [ -n "$access_key_name" ]; then if (iam_delete_access_key -u "$user_name" -k "$access_key_name"); then echo "Deleted access key $access_key_name" diff --git a/aws-cli/bash-linux/iam/iam_operations.sh b/aws-cli/bash-linux/iam/iam_operations.sh index 73a0e03db08..0b57cd9ec86 100644 --- a/aws-cli/bash-linux/iam/iam_operations.sh +++ b/aws-cli/bash-linux/iam/iam_operations.sh @@ -784,6 +784,123 @@ function iam_delete_role() { } # snippet-end:[aws-cli.bash-linux.iam.DeleteRole] +# snippet-start:[aws-cli.bash-linux.iam.UpdateAccessKey] +############################################################################### +# function iam_update_access_key +# +# This function can activate or deactivate an IAM access key for the specified IAM user. +# +# Parameters: +# -u user_name -- The name of the user. +# -k access_key -- The access key to update. +# -a -- Activate the selected access key. +# -d -- Deactivate the selected access key. +# +# Example: +# # To deactivate the selected access key for IAM user Bob +# iam_update_access_key -u Bob -k AKIAIOSFODNN7EXAMPLE -d +# +# Returns: +# 0 - If successful. +# 1 - If it fails. +############################################################################### +function iam_update_access_key() { + local user_name access_key status response + local option OPTARG # Required to use getopts command in a function. + local activate_flag=false deactivate_flag=false + + # bashsupport disable=BP5008 + function usage() { + echo "function iam_update_access_key" + echo "Updates the status of an AWS Identity and Access Management (IAM) access key for the specified IAM user" + echo " -u user_name The name of the user." + echo " -k access_key The access key to update." + echo " -a Activate the access key." + echo " -d Deactivate the access key." + echo "" + } + + # Retrieve the calling parameters. + while getopts "u:k:adh" option; do + case "${option}" in + u) user_name="${OPTARG}" ;; + k) access_key="${OPTARG}" ;; + a) activate_flag=true ;; + d) deactivate_flag=true ;; + h) + usage + return 0 + ;; + \?) + echo "Invalid parameter" + usage + return 1 + ;; + esac + done + export OPTIND=1 + + # Validate input parameters + if [[ -z "$user_name" ]]; then + errecho "ERROR: You must provide a username with the -u parameter." + usage + return 1 + fi + + if [[ -z "$access_key" ]]; then + errecho "ERROR: You must provide an access key with the -k parameter." + usage + return 1 + fi + + # Ensure that only -a or -d is specified + if [[ "$activate_flag" == true && "$deactivate_flag" == true ]]; then + errecho "ERROR: You cannot specify both -a (activate) and -d (deactivate) at the same time." + usage + return 1 + fi + + # If neither -a nor -d is provided, return an error + if [[ "$activate_flag" == false && "$deactivate_flag" == false ]]; then + errecho "ERROR: You must specify either -a (activate) or -d (deactivate)." + usage + return 1 + fi + + # Determine the status based on the flag + if [[ "$activate_flag" == true ]]; then + status="Active" + elif [[ "$deactivate_flag" == true ]]; then + status="Inactive" + fi + + iecho "Parameters:\n" + iecho " Username: $user_name" + iecho " Access key: $access_key" + iecho " New status: $status" + iecho "" + + # Update the access key status + response=$(aws iam update-access-key \ + --user-name "$user_name" \ + --access-key-id "$access_key" \ + --status "$status" 2>&1) + + local error_code=${?} + + if [[ $error_code -ne 0 ]]; then + aws_cli_error_log $error_code + errecho "ERROR: AWS reports update-access-key operation failed.\n$response" + return 1 + fi + + iecho "update-access-key response: $response" + iecho + + return 0 +} +# snippet-end:[aws-cli.bash-linux.iam.UpdateAccessKey] + # snippet-start:[aws-cli.bash-linux.iam.DeleteAccessKey] ############################################################################### # function iam_delete_access_key From 9b338550d241ad947d0f64ecd6e5f8d41d4900a1 Mon Sep 17 00:00:00 2001 From: Brian Murray <40031786+brmur@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:33:33 +0000 Subject: [PATCH 050/144] Adding Conditional Request S3 scenario and actions (JavaScriptV3) (#7215) --- .doc_gen/metadata/s3_metadata.yaml | 50 ++ .../bedrock-agent-runtime/package.json | 2 +- .../example_code/bedrock-agent/package.json | 3 +- .../example_code/bedrock-runtime/package.json | 2 +- .../example_code/bedrock/package.json | 2 +- .../cloudwatch-events/package.json | 2 +- .../example_code/cloudwatch-logs/package.json | 4 +- .../example_code/cloudwatch/package.json | 2 +- .../example_code/codebuild/package.json | 2 +- .../example_code/codecommit/package.json | 3 +- .../cognito-identity-provider/package.json | 4 +- .../aurora-serverless-app/package.json | 2 +- .../AnalyzeSentiment/package.json | 2 +- .../ExtractText/package.json | 2 +- .../SynthesizeAudio/package.json | 2 +- .../TranslateText/package.json | 2 +- .../photo-asset-manager/package.json | 2 +- .../wkflw-pools-triggers/cdk/package.json | 2 +- .../wkflw-pools-triggers/package.json | 2 +- .../wkflw-resilient-service/package.json | 2 +- .../wkflw-topics-queues/package.json | 2 +- .../example_code/dynamodb/package.json | 2 +- javascriptv3/example_code/ec2/package.json | 2 +- .../elastic-load-balancing-v2/package.json | 2 +- .../example_code/eventbridge/package.json | 2 +- javascriptv3/example_code/glue/package.json | 4 +- javascriptv3/example_code/iam/package.json | 2 +- .../example_code/iotsitewise/package.json | 4 +- .../example_code/kinesis/package.json | 2 +- javascriptv3/example_code/lambda/package.json | 4 +- javascriptv3/example_code/libs/package.json | 2 +- .../example_code/medical-imaging/package.json | 4 +- .../example_code/nodegetstarted/README.md | 2 +- .../example_code/nodegetstarted/package.json | 2 +- .../example_code/personalize/package.json | 2 +- javascriptv3/example_code/s3/README.md | 15 +- ...opy-object-conditional-request-if-match.js | 91 ++++ ...t-conditional-request-if-modified-since.js | 92 ++++ ...bject-conditional-request-if-none-match.js | 92 ++++ ...conditional-request-if-unmodified-since.js | 91 ++++ ...get-object-conditional-request-if-match.js | 78 ++++ ...t-conditional-request-if-modified-since.js | 75 +++ ...bject-conditional-request-if-none-match.js | 78 ++++ ...conditional-request-if-unmodified-since.js | 75 +++ ...bject-conditional-request-if-none-match.js | 67 +++ .../example_code/s3/actions/text01.txt | 1 + javascriptv3/example_code/s3/package.json | 4 +- .../scenarios/conditional-requests/.gitignore | 1 + .../scenarios/conditional-requests/README.md | 64 +++ .../conditional-requests/clean.steps.js | 69 +++ .../clean.steps.unit.test.js | 44 ++ .../conditional-requests.integration.test.js | 37 ++ .../scenarios/conditional-requests/index.js | 81 ++++ .../conditional-requests/object_name.json | 3 + .../repl.steps.integration.test.js | 16 + .../conditional-requests/repl.steps.js | 439 ++++++++++++++++++ .../conditional-requests/setup.steps.js | 146 ++++++ .../scenarios/conditional-requests/text02.txt | 0 .../conditional-requests/welcome.steps.js | 36 ++ .../object-locking/clean.steps.unit.test.js | 2 +- .../object-locking/index.unit.test.js | 2 +- .../object-locking/repl.steps.unit.test.js | 6 +- .../object-locking/setup.steps.unit.test.js | 10 +- ...ional-request-if-match.integration.test.js | 20 + ...uest-if-modified-since.integration.test.js | 19 + ...-request-if-none-match.integration.test.js | 19 + ...st-if-unmodified-since.integration.test.js | 19 + ...ional-request-if-match.integration.test.js | 19 + ...uest-if-modified-since.integration.test.js | 18 + ...-request-if-none-match.integration.test.js | 19 + ...st-if-unmodified-since.integration.test.js | 18 + ...-request-if-none-match.integration.test.js | 17 + javascriptv3/example_code/s3/tests/text01.txt | 1 + javascriptv3/example_code/s3/text01.txt | 1 + .../example_code/secrets-manager/package.json | 2 +- javascriptv3/example_code/ses/package.json | 2 +- javascriptv3/example_code/sfn/package.json | 2 +- javascriptv3/example_code/sns/package.json | 2 +- javascriptv3/example_code/sqs/package.json | 2 +- javascriptv3/example_code/ssm/package.json | 4 +- javascriptv3/example_code/sts/package.json | 2 +- .../example_code/support/package.json | 2 +- 82 files changed, 1972 insertions(+), 63 deletions(-) create mode 100644 javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-match.js create mode 100644 javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-modified-since.js create mode 100644 javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-none-match.js create mode 100644 javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-unmodified-since.js create mode 100644 javascriptv3/example_code/s3/actions/get-object-conditional-request-if-match.js create mode 100644 javascriptv3/example_code/s3/actions/get-object-conditional-request-if-modified-since.js create mode 100644 javascriptv3/example_code/s3/actions/get-object-conditional-request-if-none-match.js create mode 100644 javascriptv3/example_code/s3/actions/get-object-conditional-request-if-unmodified-since.js create mode 100644 javascriptv3/example_code/s3/actions/put-object-conditional-request-if-none-match.js create mode 100644 javascriptv3/example_code/s3/actions/text01.txt create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/.gitignore create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/README.md create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.unit.test.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/conditional-requests.integration.test.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/index.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/object_name.json create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.integration.test.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/setup.steps.js create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/text02.txt create mode 100644 javascriptv3/example_code/s3/scenarios/conditional-requests/welcome.steps.js create mode 100644 javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-match.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-modified-since.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-none-match.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-unmodified-since.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/get-object-conditional-request-if-match.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/get-object-conditional-request-if-modified-since.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/get-object-conditional-request-if-none-match.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/get-object-conditional-request-if-unmodified-since.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/put-object-conditional-request-if-none-match.integration.test.js create mode 100644 javascriptv3/example_code/s3/tests/text01.txt create mode 100644 javascriptv3/example_code/s3/text01.txt diff --git a/.doc_gen/metadata/s3_metadata.yaml b/.doc_gen/metadata/s3_metadata.yaml index 061b269d3dd..8569428e2cd 100644 --- a/.doc_gen/metadata/s3_metadata.yaml +++ b/.doc_gen/metadata/s3_metadata.yaml @@ -293,6 +293,18 @@ s3_CopyObject: - description: Copy the object. snippet_tags: - s3.JavaScript.buckets.copyObjectV3 + - description: Copy the object on condition its ETag does not match the one provided. + snippet_files: + - javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-match.js + - description: Copy the object on condition its ETag does not match the one provided. + snippet_files: + - javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-none-match.js + - description: Copy the object using on condition it has been created or modified in a given timeframe. + snippet_files: + - javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-modified-since.js + - description: Copy the object using on condition it has not been created or modified in a given timeframe. + snippet_files: + - javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-unmodified-since.js PHP: versions: - sdk_version: 3 @@ -951,6 +963,18 @@ s3_GetObject: - description: Download the object. snippet_tags: - s3.JavaScript.buckets.getobjectV3 + - description: Download the object on condition its ETag does not match the one provided. + snippet_files: + - javascriptv3/example_code/s3/actions/get-object-conditional-request-if-match.js + - description: Download the object on condition its ETag does not match the one provided. + snippet_files: + - javascriptv3/example_code/s3/actions/get-object-conditional-request-if-none-match.js + - description: Download the object using on condition it has been created or modified in a given timeframe. + snippet_files: + - javascriptv3/example_code/s3/actions/get-object-conditional-request-if-modified-since.js + - description: Download the object using on condition it has not been created or modified in a given timeframe. + snippet_files: + - javascriptv3/example_code/s3/actions/get-object-conditional-request-if-unmodified-since.js Ruby: versions: - sdk_version: 3 @@ -1602,6 +1626,9 @@ s3_PutObject: - description: Upload the object. snippet_tags: - s3.JavaScript.buckets.uploadV3 + - description: Upload the object on condition its ETag matches the one provided. + snippet_files: + - javascriptv3/example_code/s3/actions/get-object-conditional-request-if-match.js Ruby: versions: - sdk_version: 3 @@ -3617,6 +3644,29 @@ s3_Scenario_ConditionalRequests: - description: A wrapper class for S3 functions. snippet_tags: - S3ConditionalRequests.dotnetv3.S3ActionsWrapper + JavaScript: + versions: + - sdk_version: 3 + github: javascriptv3/example_code/s3/scenarios/conditional-requests + sdkguide: + excerpts: + - description: | + Entrypoint for the workflow (index.js). This orchestrates all of the steps. + Visit GitHub to see the implementation details for Scenario, ScenarioInput, ScenarioOutput, and ScenarioAction. + snippet_files: + - javascriptv3/example_code/s3/scenarios/conditional-requests/index.js + - description: Output welcome messages to the console (welcome.steps.js). + snippet_files: + - javascriptv3/example_code/s3/scenarios/conditional-requests/welcome.steps.js + - description: Deploy buckets and objects (setup.steps.js). + snippet_files: + - javascriptv3/example_code/s3/scenarios/conditional-requests/setup.steps.js + - description: Get, copy, and put objects using S3 conditional requests (repl.steps.js). + snippet_files: + - javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.js + - description: Destroy all created resources (clean.steps.js). + snippet_files: + - javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.js services: s3: {GetObject, PutObject, CopyObject} s3_Scenario_DownloadS3Directory: diff --git a/javascriptv3/example_code/bedrock-agent-runtime/package.json b/javascriptv3/example_code/bedrock-agent-runtime/package.json index 44a3a43bb4a..ec65b348886 100644 --- a/javascriptv3/example_code/bedrock-agent-runtime/package.json +++ b/javascriptv3/example_code/bedrock-agent-runtime/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/bedrock-agent-runtime-test-results.junit.xml" }, "dependencies": { "@aws-sdk/client-bedrock-agent-runtime": "^3.675.0" diff --git a/javascriptv3/example_code/bedrock-agent/package.json b/javascriptv3/example_code/bedrock-agent/package.json index d3280ea23f3..9e4a6950faa 100644 --- a/javascriptv3/example_code/bedrock-agent/package.json +++ b/javascriptv3/example_code/bedrock-agent/package.json @@ -5,8 +5,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/bedrock-agent-test-results.junit.xml" }, "dependencies": { "@aws-sdk/client-bedrock-agent": "^3.515.0" diff --git a/javascriptv3/example_code/bedrock-runtime/package.json b/javascriptv3/example_code/bedrock-runtime/package.json index 25e81ad8de2..dba0f51ad9c 100644 --- a/javascriptv3/example_code/bedrock-runtime/package.json +++ b/javascriptv3/example_code/bedrock-runtime/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/bedrock-runtime-test-results.junit.xml" }, "devDependencies": { "vitest": "^1.6.0" diff --git a/javascriptv3/example_code/bedrock/package.json b/javascriptv3/example_code/bedrock/package.json index 21ec6fdb75d..9ca3dc2f322 100644 --- a/javascriptv3/example_code/bedrock/package.json +++ b/javascriptv3/example_code/bedrock/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/bedrock-test-results.junit.xml" }, "dependencies": { "@aws-sdk/client-bedrock": "^3.485.0" diff --git a/javascriptv3/example_code/cloudwatch-events/package.json b/javascriptv3/example_code/cloudwatch-events/package.json index 9e500762b11..ff3c03fa6c4 100644 --- a/javascriptv3/example_code/cloudwatch-events/package.json +++ b/javascriptv3/example_code/cloudwatch-events/package.json @@ -11,7 +11,7 @@ }, "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/cloudwatchevents-test-results.junit.xml" }, "devDependencies": { "vitest": "^1.6.0" diff --git a/javascriptv3/example_code/cloudwatch-logs/package.json b/javascriptv3/example_code/cloudwatch-logs/package.json index 0c529bb1821..3ec85489167 100644 --- a/javascriptv3/example_code/cloudwatch-logs/package.json +++ b/javascriptv3/example_code/cloudwatch-logs/package.json @@ -11,8 +11,8 @@ "@aws-sdk/client-lambda": "^3.216.0" }, "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml", - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/cloudwatchlogs-test-results.junit.xml" }, "devDependencies": { "vitest": "^1.6.0" diff --git a/javascriptv3/example_code/cloudwatch/package.json b/javascriptv3/example_code/cloudwatch/package.json index 3466ca5e2a5..43152195365 100644 --- a/javascriptv3/example_code/cloudwatch/package.json +++ b/javascriptv3/example_code/cloudwatch/package.json @@ -10,7 +10,7 @@ "@aws-sdk/client-ec2": "^3.213.0" }, "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/cloudwatch-test-results.junit.xml" }, "devDependencies": { "uuid": "^9.0.0", diff --git a/javascriptv3/example_code/codebuild/package.json b/javascriptv3/example_code/codebuild/package.json index de3b34e3043..68e31086a7d 100644 --- a/javascriptv3/example_code/codebuild/package.json +++ b/javascriptv3/example_code/codebuild/package.json @@ -9,7 +9,7 @@ }, "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/codebuild-test-results.junit.xml" }, "devDependencies": { "@aws-sdk/client-iam": "^3.391.0", diff --git a/javascriptv3/example_code/codecommit/package.json b/javascriptv3/example_code/codecommit/package.json index 02cebf4a042..fc12459865e 100644 --- a/javascriptv3/example_code/codecommit/package.json +++ b/javascriptv3/example_code/codecommit/package.json @@ -5,7 +5,8 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/codecommit-test-results.junit.xml" }, "dependencies": { "@aws-sdk/client-codecommit": "^3.427.0" diff --git a/javascriptv3/example_code/cognito-identity-provider/package.json b/javascriptv3/example_code/cognito-identity-provider/package.json index 0b581ecf36f..f3c8928c5be 100644 --- a/javascriptv3/example_code/cognito-identity-provider/package.json +++ b/javascriptv3/example_code/cognito-identity-provider/package.json @@ -7,8 +7,8 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/cognito-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/cross-services/aurora-serverless-app/package.json b/javascriptv3/example_code/cross-services/aurora-serverless-app/package.json index 2704310637c..085cfa4eff2 100644 --- a/javascriptv3/example_code/cross-services/aurora-serverless-app/package.json +++ b/javascriptv3/example_code/cross-services/aurora-serverless-app/package.json @@ -5,7 +5,7 @@ "type": "module", "main": "build/index.js", "scripts": { - "test": "vitest run **/*.unit.test.ts", + "test": "vitest run unit", "start": "node ./watch.js" }, "author": "corepyle@amazon.com", diff --git a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/AnalyzeSentiment/package.json b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/AnalyzeSentiment/package.json index 172f8e9f1cc..047a6923641 100644 --- a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/AnalyzeSentiment/package.json +++ b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/AnalyzeSentiment/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", + "test": "vitest run unit", "build": "rollup -c" }, "author": "Corey Pyle ", diff --git a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/ExtractText/package.json b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/ExtractText/package.json index 791fa7de51e..988a7bc54a1 100644 --- a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/ExtractText/package.json +++ b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/ExtractText/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", + "test": "vitest run unit", "build": "rollup -c" }, "author": "Corey Pyle ", diff --git a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/SynthesizeAudio/package.json b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/SynthesizeAudio/package.json index b2b992fd2fb..24373853a16 100644 --- a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/SynthesizeAudio/package.json +++ b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/SynthesizeAudio/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", + "test": "vitest run unit", "build": "rollup -c" }, "author": "Corey Pyle ", diff --git a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/TranslateText/package.json b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/TranslateText/package.json index db59ed6f82a..61d44f844c4 100644 --- a/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/TranslateText/package.json +++ b/javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/TranslateText/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", + "test": "vitest run unit", "build": "rollup -c" }, "author": "Corey Pyle ", diff --git a/javascriptv3/example_code/cross-services/photo-asset-manager/package.json b/javascriptv3/example_code/cross-services/photo-asset-manager/package.json index 22191ba173e..3ac3a52ea67 100644 --- a/javascriptv3/example_code/cross-services/photo-asset-manager/package.json +++ b/javascriptv3/example_code/cross-services/photo-asset-manager/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "build": "rollup -c", - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "author": "Corey Pyle ", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/cross-services/wkflw-pools-triggers/cdk/package.json b/javascriptv3/example_code/cross-services/wkflw-pools-triggers/cdk/package.json index 1811921dfff..af2363eab58 100644 --- a/javascriptv3/example_code/cross-services/wkflw-pools-triggers/cdk/package.json +++ b/javascriptv3/example_code/cross-services/wkflw-pools-triggers/cdk/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "watch": "tsc -w", - "test": "vitest run **/*.unit.test.ts", + "test": "vitest run unit", "cdk": "cdk" }, "devDependencies": { diff --git a/javascriptv3/example_code/cross-services/wkflw-pools-triggers/package.json b/javascriptv3/example_code/cross-services/wkflw-pools-triggers/package.json index 9b3196d9b06..eacfff7e5b9 100644 --- a/javascriptv3/example_code/cross-services/wkflw-pools-triggers/package.json +++ b/javascriptv3/example_code/cross-services/wkflw-pools-triggers/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "test": "npm run cdk-test", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/wkflw-pools-triggers-test-results.junit.xml", "cdk-test": "npm run test --prefix ./cdk" }, "engines": { diff --git a/javascriptv3/example_code/cross-services/wkflw-resilient-service/package.json b/javascriptv3/example_code/cross-services/wkflw-resilient-service/package.json index a5e6f99b238..3dd601fa0d6 100644 --- a/javascriptv3/example_code/cross-services/wkflw-resilient-service/package.json +++ b/javascriptv3/example_code/cross-services/wkflw-resilient-service/package.json @@ -6,7 +6,7 @@ "author": "Corey Pyle ", "license": "Apache-2.0", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/wkflw-resilient-service-test-results.junit.xml" }, "dependencies": { "@aws-sdk/client-auto-scaling": "^3.438.0", diff --git a/javascriptv3/example_code/cross-services/wkflw-topics-queues/package.json b/javascriptv3/example_code/cross-services/wkflw-topics-queues/package.json index 1cec553bc24..02579945eac 100644 --- a/javascriptv3/example_code/cross-services/wkflw-topics-queues/package.json +++ b/javascriptv3/example_code/cross-services/wkflw-topics-queues/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "author": "Corey Pyle ", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/dynamodb/package.json b/javascriptv3/example_code/dynamodb/package.json index b2240caf2e5..dcd2362269e 100644 --- a/javascriptv3/example_code/dynamodb/package.json +++ b/javascriptv3/example_code/dynamodb/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/dynamodb-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/ec2/package.json b/javascriptv3/example_code/ec2/package.json index 1a4c43b968c..76afded06b4 100644 --- a/javascriptv3/example_code/ec2/package.json +++ b/javascriptv3/example_code/ec2/package.json @@ -5,7 +5,7 @@ "license": "Apache 2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/elastic-load-balancing-v2/package.json b/javascriptv3/example_code/elastic-load-balancing-v2/package.json index 08d56c7b16a..6f26b154582 100644 --- a/javascriptv3/example_code/elastic-load-balancing-v2/package.json +++ b/javascriptv3/example_code/elastic-load-balancing-v2/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/elastic-load-balancing-test-results.junit.xml" }, "author": "Corey Pyle ", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/eventbridge/package.json b/javascriptv3/example_code/eventbridge/package.json index 6c7d9736f00..b6a3bc3a70b 100644 --- a/javascriptv3/example_code/eventbridge/package.json +++ b/javascriptv3/example_code/eventbridge/package.json @@ -4,7 +4,7 @@ "author": "Corey Pyle ", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/eventbridge-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/glue/package.json b/javascriptv3/example_code/glue/package.json index b771b70b889..1dd662954b1 100644 --- a/javascriptv3/example_code/glue/package.json +++ b/javascriptv3/example_code/glue/package.json @@ -6,8 +6,8 @@ "author": "Corey Pyle ", "license": "Apache-2.0", "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/glue-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.1", diff --git a/javascriptv3/example_code/iam/package.json b/javascriptv3/example_code/iam/package.json index 067e6c55a01..03416d54973 100644 --- a/javascriptv3/example_code/iam/package.json +++ b/javascriptv3/example_code/iam/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/iam-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/iotsitewise/package.json b/javascriptv3/example_code/iotsitewise/package.json index 15f618aac7c..2b89b43a002 100644 --- a/javascriptv3/example_code/iotsitewise/package.json +++ b/javascriptv3/example_code/iotsitewise/package.json @@ -6,8 +6,8 @@ "test": "tests" }, "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/sitewise-test-results.junit.xml" }, "author": "beqqrry@amazon.com", "license": "ISC", diff --git a/javascriptv3/example_code/kinesis/package.json b/javascriptv3/example_code/kinesis/package.json index f270994479a..2f69750ed40 100644 --- a/javascriptv3/example_code/kinesis/package.json +++ b/javascriptv3/example_code/kinesis/package.json @@ -5,7 +5,7 @@ "test": "tests" }, "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/kinesis-test-results.junit.xml" }, "author": "Corey Pyle ", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/lambda/package.json b/javascriptv3/example_code/lambda/package.json index d93a590f7a0..1e67faa3bad 100644 --- a/javascriptv3/example_code/lambda/package.json +++ b/javascriptv3/example_code/lambda/package.json @@ -7,8 +7,8 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/lambda-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/libs/package.json b/javascriptv3/example_code/libs/package.json index ab8ea4369e7..5700112fffb 100644 --- a/javascriptv3/example_code/libs/package.json +++ b/javascriptv3/example_code/libs/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "dependencies": { "@aws-sdk/client-cloudformation": "^3.637.0", diff --git a/javascriptv3/example_code/medical-imaging/package.json b/javascriptv3/example_code/medical-imaging/package.json index 72e664b221f..004b1f67a3e 100644 --- a/javascriptv3/example_code/medical-imaging/package.json +++ b/javascriptv3/example_code/medical-imaging/package.json @@ -10,8 +10,8 @@ "@aws-sdk/client-sts": "^3.620.0" }, "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/medical-imaging-test-results.junit.xml" }, "type": "module", "devDependencies": { diff --git a/javascriptv3/example_code/nodegetstarted/README.md b/javascriptv3/example_code/nodegetstarted/README.md index 5d22e77b2b9..ee2eb08ef08 100644 --- a/javascriptv3/example_code/nodegetstarted/README.md +++ b/javascriptv3/example_code/nodegetstarted/README.md @@ -38,7 +38,7 @@ The final package.json should look similar to this: "description": "This guide shows you how to initialize an NPM package, add a service client to your package, and use the JavaScript SDK to call a service action.", "main": "index.js", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "author": "Corey Pyle ", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/nodegetstarted/package.json b/javascriptv3/example_code/nodegetstarted/package.json index ddbcf14efd7..bea0152cec0 100644 --- a/javascriptv3/example_code/nodegetstarted/package.json +++ b/javascriptv3/example_code/nodegetstarted/package.json @@ -4,7 +4,7 @@ "description": "This guide shows you how to initialize an NPM package, add a service client to your package, and use the JavaScript SDK to call a service action.", "main": "index.js", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/javascriptv3-get-started-node-test-results.junit.xml" }, "author": "Corey Pyle ", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/personalize/package.json b/javascriptv3/example_code/personalize/package.json index f8903f776fe..2f0d59abe8b 100644 --- a/javascriptv3/example_code/personalize/package.json +++ b/javascriptv3/example_code/personalize/package.json @@ -4,7 +4,7 @@ "description": "personalize operations", "main": "personalizeClients.js", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "type": "module", "author": "Samuel Ashman ", diff --git a/javascriptv3/example_code/s3/README.md b/javascriptv3/example_code/s3/README.md index f352d4c36da..7afa1f9a74f 100644 --- a/javascriptv3/example_code/s3/README.md +++ b/javascriptv3/example_code/s3/README.md @@ -80,6 +80,7 @@ functions within the same service. - [Create a web page that lists Amazon S3 objects](../web/s3/list-objects/src/App.tsx) - [Delete all objects in a bucket](scenarios/delete-all-objects.js) - [Lock Amazon S3 objects](scenarios/object-locking/index.js) +- [Make conditional requests](scenarios/conditional-requests/index.js) - [Upload or download large files](scenarios/multipart-upload.js) @@ -200,6 +201,18 @@ This example shows you how to work with S3 object lock features. +#### Make conditional requests + +This example shows you how to add preconditions to Amazon S3 requests. + + + + + + + + + #### Upload or download large files This example shows you how to upload or download large files to and from Amazon S3. @@ -238,4 +251,4 @@ in the `javascriptv3` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-match.js b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-match.js new file mode 100644 index 00000000000..93495fb5914 --- /dev/null +++ b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-match.js @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CopyObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +// Optionally edit the default key name of the copied object in 'object_name.json' +import data from "../scenarios/conditional-requests/object_name.json" assert { + type: "json", +}; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ sourceBucketName: string, sourceKeyName: string, destinationBucketName: string, eTag: string }} + */ +export const main = async ({ + sourceBucketName, + sourceKeyName, + destinationBucketName, + eTag, +}) => { + const client = new S3Client({}); + const name = data.name; + try { + const response = await client.send( + new CopyObjectCommand({ + CopySource: `${sourceBucketName}/${sourceKeyName}`, + Bucket: destinationBucketName, + Key: `${name}${sourceKeyName}`, + CopySourceIfMatch: eTag, + }), + ); + console.log("Successfully copied object to bucket."); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while copying object "${sourceKeyName}" from "${sourceBucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Unable to copy object "${sourceKeyName}" to bucket "${sourceBucketName}": ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + sourceBucketName: { + type: "string", + required: true, + }, + sourceKeyName: { + type: "string", + required: true, + }, + destinationBucketName: { + type: "string", + required: true, + }, + eTag: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-modified-since.js b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-modified-since.js new file mode 100644 index 00000000000..8f3cdfa5363 --- /dev/null +++ b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-modified-since.js @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CopyObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +// Optionally edit the default key name of the copied object in 'object_name.json' +import data from "../scenarios/conditional-requests/object_name.json" assert { + type: "json", +}; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ sourceBucketName: string, sourceKeyName: string, destinationBucketName: string }} + */ +export const main = async ({ + sourceBucketName, + sourceKeyName, + destinationBucketName, +}) => { + const date = new Date(); + date.setDate(date.getDate() - 1); + + const name = data.name; + const client = new S3Client({}); + const copySource = `${sourceBucketName}/${sourceKeyName}`; + const copiedKey = name + sourceKeyName; + + try { + const response = await client.send( + new CopyObjectCommand({ + CopySource: copySource, + Bucket: destinationBucketName, + Key: copiedKey, + CopySourceIfModifiedSince: date, + }), + ); + console.log("Successfully copied object to bucket."); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while copying object "${sourceKeyName}" from "${sourceBucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while copying object from ${sourceBucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + sourceBucketName: { + type: "string", + required: true, + }, + sourceKeyName: { + type: "string", + required: true, + }, + destinationBucketName: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-none-match.js b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-none-match.js new file mode 100644 index 00000000000..d4aed2f1e01 --- /dev/null +++ b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-none-match.js @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CopyObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +// Optionally edit the default key name of the copied object in 'object_name.json' +import data from "../scenarios/conditional-requests/object_name.json" assert { + type: "json", +}; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ sourceBucketName: string, sourceKeyName: string, destinationBucketName: string, eTag: string }} + */ +export const main = async ({ + sourceBucketName, + sourceKeyName, + destinationBucketName, + eTag, +}) => { + const client = new S3Client({}); + const name = data.name; + + try { + const response = await client.send( + new CopyObjectCommand({ + CopySource: `${sourceBucketName}/${sourceKeyName}`, + Bucket: destinationBucketName, + Key: `${name}${sourceKeyName}`, + CopySourceIfNoneMatch: eTag, + }), + ); + console.log("Successfully copied object to bucket."); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while copying object "${sourceKeyName}" from "${sourceBucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Unable to copy object "${sourceKeyName}" to bucket "${sourceBucketName}": ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + sourceBucketName: { + type: "string", + required: true, + }, + sourceKeyName: { + type: "string", + required: true, + }, + destinationBucketName: { + type: "string", + required: true, + }, + eTag: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-unmodified-since.js b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-unmodified-since.js new file mode 100644 index 00000000000..5ffee11f44b --- /dev/null +++ b/javascriptv3/example_code/s3/actions/copy-object-conditional-request-if-unmodified-since.js @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CopyObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +// Optionally edit the default key name of the copied object in 'object_name.json' +import data from "../scenarios/conditional-requests/object_name.json" assert { + type: "json", +}; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ sourceBucketName: string, sourceKeyName: string, destinationBucketName: string }} + */ +export const main = async ({ + sourceBucketName, + sourceKeyName, + destinationBucketName, +}) => { + const date = new Date(); + date.setDate(date.getDate() - 1); + const client = new S3Client({}); + const name = data.name; + const copiedKey = name + sourceKeyName; + const copySource = `${sourceBucketName}/${sourceKeyName}`; + + try { + const response = await client.send( + new CopyObjectCommand({ + CopySource: copySource, + Bucket: destinationBucketName, + Key: copiedKey, + CopySourceIfUnmodifiedSince: date, + }), + ); + console.log("Successfully copied object to bucket."); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while copying object "${sourceKeyName}" from "${sourceBucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while copying object from ${sourceBucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + sourceBucketName: { + type: "string", + required: true, + }, + sourceKeyName: { + type: "string", + required: true, + }, + destinationBucketName: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-match.js b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-match.js new file mode 100644 index 00000000000..2720e21f069 --- /dev/null +++ b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-match.js @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + GetObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ bucketName: string, key: string, eTag: string }} + */ +export const main = async ({ bucketName, key, eTag }) => { + const client = new S3Client({}); + + try { + const response = await client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: key, + IfMatch: eTag, + }), + ); + // The Body object also has 'transformToByteArray' and 'transformToWebStream' methods. + const str = await response.Body.transformToString(); + console.log("Success. Here is text of the file:", str); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while getting object "${key}" from "${bucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while getting object from ${bucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + bucketName: { + type: "string", + required: true, + }, + key: { + type: "string", + required: true, + }, + eTag: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-modified-since.js b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-modified-since.js new file mode 100644 index 00000000000..d51688f8aac --- /dev/null +++ b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-modified-since.js @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + GetObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ bucketName: string, key: string }} + */ +export const main = async ({ bucketName, key }) => { + const client = new S3Client({}); + const date = new Date(); + date.setDate(date.getDate() - 1); + try { + const response = await client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: key, + IfModifiedSince: date, + }), + ); + // The Body object also has 'transformToByteArray' and 'transformToWebStream' methods. + const str = await response.Body.transformToString(); + console.log("Success. Here is text of the file:", str); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while getting object "${key}" from "${bucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while getting object from ${bucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + bucketName: { + type: "string", + required: true, + }, + key: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-none-match.js b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-none-match.js new file mode 100644 index 00000000000..10258ee07ce --- /dev/null +++ b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-none-match.js @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + GetObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ bucketName: string, key: string, eTag: string }} + */ +export const main = async ({ bucketName, key, eTag }) => { + const client = new S3Client({}); + + try { + const response = await client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: key, + IfNoneMatch: eTag, + }), + ); + // The Body object also has 'transformToByteArray' and 'transformToWebStream' methods. + const str = await response.Body.transformToString(); + console.log("Success. Here is text of the file:", str); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while getting object "${key}" from "${bucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while getting object from ${bucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + bucketName: { + type: "string", + required: true, + }, + key: { + type: "string", + required: true, + }, + eTag: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-unmodified-since.js b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-unmodified-since.js new file mode 100644 index 00000000000..a17b94c7b89 --- /dev/null +++ b/javascriptv3/example_code/s3/actions/get-object-conditional-request-if-unmodified-since.js @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + GetObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ bucketName: string, key: string }} + */ +export const main = async ({ bucketName, key }) => { + const client = new S3Client({}); + const date = new Date(); + date.setDate(date.getDate() - 1); + try { + const response = await client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: key, + IfUnmodifiedSince: date, + }), + ); + // The Body object also has 'transformToByteArray' and 'transformToWebStream' methods. + const str = await response.Body.transformToString(); + console.log("Success. Here is text of the file:", str); + } catch (caught) { + if (caught instanceof NoSuchKey) { + console.error( + `Error from S3 while getting object "${key}" from "${bucketName}". No such key exists.`, + ); + } else if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while getting object from ${bucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + bucketName: { + type: "string", + required: true, + }, + key: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/put-object-conditional-request-if-none-match.js b/javascriptv3/example_code/s3/actions/put-object-conditional-request-if-none-match.js new file mode 100644 index 00000000000..0583b016a3d --- /dev/null +++ b/javascriptv3/example_code/s3/actions/put-object-conditional-request-if-none-match.js @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + PutObjectCommand, + S3Client, + S3ServiceException, +} from "@aws-sdk/client-s3"; +import { readFile } from "node:fs/promises"; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ destinationBucketName: string }} + */ +export const main = async ({ destinationBucketName }) => { + const client = new S3Client({}); + const filePath = "./text01.txt"; + try { + await client.send( + new PutObjectCommand({ + Bucket: destinationBucketName, + Key: "text01.txt", + Body: await readFile(filePath), + IfNoneMatch: "*", + }), + ); + console.log( + "File written to bucket because the key name is not a duplicate.", + ); + } catch (caught) { + if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while uploading object to bucket. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +// Call function if run directly +import { parseArgs } from "node:util"; +import { + isMain, + validateArgs, +} from "@aws-doc-sdk-examples/lib/utils/util-node.js"; + +const loadArgs = () => { + const options = { + destinationBucketName: { + type: "string", + required: true, + }, + }; + const results = parseArgs({ options }); + const { errors } = validateArgs({ options }, results); + return { errors, results }; +}; + +if (isMain(import.meta.url)) { + const { errors, results } = loadArgs(); + if (!errors) { + main(results.values); + } else { + console.error(errors.join("\n")); + } +} diff --git a/javascriptv3/example_code/s3/actions/text01.txt b/javascriptv3/example_code/s3/actions/text01.txt new file mode 100644 index 00000000000..11e519d1129 --- /dev/null +++ b/javascriptv3/example_code/s3/actions/text01.txt @@ -0,0 +1 @@ +This is a sample text file for use in some action examples in this folder. \ No newline at end of file diff --git a/javascriptv3/example_code/s3/package.json b/javascriptv3/example_code/s3/package.json index 98d8ca23f58..4733159067c 100644 --- a/javascriptv3/example_code/s3/package.json +++ b/javascriptv3/example_code/s3/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "Examples demonstrating how to use the AWS SDK for JavaScript (v3) to interact with Amazon S3.", "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/s3-test-results.junit.xml" }, "author": "corepyle@amazon.com", "license": "Apache-2.0", diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/.gitignore b/javascriptv3/example_code/s3/scenarios/conditional-requests/.gitignore new file mode 100644 index 00000000000..b7887cb1903 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/.gitignore @@ -0,0 +1 @@ +state.json \ No newline at end of file diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/README.md b/javascriptv3/example_code/s3/scenarios/conditional-requests/README.md new file mode 100644 index 00000000000..6fb4f7558c2 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/README.md @@ -0,0 +1,64 @@ +# Amazon S3 Conditional Requests Feature Scenario for the SDK for JavaScript (v3) + +## Overview + +This example demonstrates how to use the AWS SDK for JavaScript (v3) to work with Amazon Simple Storage Service (Amazon S3) conditional request features. The scenario demonstrates how to add preconditions to S3 operations, and how those operations will succeed or fail based on the conditional requests. + +[Amazon S3 Conditional Requests](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html) are used to add preconditions to S3 read, copy, or write requests. + +## ⚠ Important + +- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../../../README.md#prerequisites) in the `javascriptv3` folder. + +### Scenarios + +This example uses a feature scenario to demonstrate various aspects of S3 conditional requests. The scenario is divided into three stages: + +1. **Deploy**: Create test buckets and objects. +2. **Demo**: Explore S3 conditional requests by listing objects, attempting to read or write with conditional requests, and viewing request results. +3. **Clean**: Delete all objects and buckets. + +#### Deploy Stage + +```bash +node index.js -s deploy +``` + +#### Demo Stage + +```bash +node index.js -s demo +``` + +#### Clean Stage + +```bash +node index.js -s clean +``` + +## 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 `javascriptv3` folder. + +## Additional resources + +- [Amazon S3 Developer Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html) +- [Amazon S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) +- [SDK for JavaScript (v3) Amazon S3 reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/index.html) + +--- + +Copyright Amazon.com, Inc. or its cd ..affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.js new file mode 100644 index 00000000000..2cb06c6b945 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.js @@ -0,0 +1,69 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + DeleteObjectCommand, + DeleteBucketCommand, + ListObjectVersionsCommand, +} from "@aws-sdk/client-s3"; + +/** + * @typedef {import("@aws-doc-sdk-examples/lib/scenario/index.js")} Scenarios + */ + +/** + * @typedef {import("@aws-sdk/client-s3").S3Client} S3Client + */ + +/** + * @param {Scenarios} scenarios + */ +const confirmCleanup = (scenarios) => + new scenarios.ScenarioInput("confirmCleanup", "Clean up resources?", { + type: "confirm", + }); + +/** + * @param {Scenarios} scenarios + * @param {S3Client} client + */ +const cleanupAction = (scenarios, client) => + new scenarios.ScenarioAction("cleanupAction", async (state) => { + const { sourceBucketName, destinationBucketName } = state; + const buckets = [sourceBucketName, destinationBucketName].filter((b) => b); + + for (const bucket of buckets) { + try { + let objectsResponse; + objectsResponse = await client.send( + new ListObjectVersionsCommand({ + Bucket: bucket, + }), + ); + for (const version of objectsResponse.Versions || []) { + const { Key, VersionId } = version; + try { + await client.send( + new DeleteObjectCommand({ + Bucket: bucket, + Key, + VersionId, + }), + ); + } catch (err) { + console.log(`An error occurred: ${err.message} `); + } + } + } catch (e) { + if (e instanceof Error && e.name === "NoSuchBucket") { + console.log("Objects and buckets have already been deleted."); + continue; + } + throw e; + } + + await client.send(new DeleteBucketCommand({ Bucket: bucket })); + console.log(`Delete for ${bucket} complete.`); + } + }); + +export { confirmCleanup, cleanupAction }; diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.unit.test.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.unit.test.js new file mode 100644 index 00000000000..c2d8ac15e29 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/clean.steps.unit.test.js @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { describe, it, expect, vi } from "vitest"; +import { ListObjectVersionsCommand } from "@aws-sdk/client-s3"; + +import * as Scenarios from "@aws-doc-sdk-examples/lib/scenario/index.js"; + +import { cleanupAction } from "./clean.steps.js"; + +describe("clean.steps.js", () => { + it("should call ListObjectVersionsCommand once for each bucket", async () => { + const mockClient = { + send: vi + .fn() + .mockResolvedValueOnce({ Versions: [] }) // ListObjectVersionsCommand + .mockResolvedValueOnce({}) // DeleteBucketCommand + .mockResolvedValueOnce({ Versions: [] }) // ListObjectVersionsCommand + .mockResolvedValueOnce({}), // DeleteBucketCommand + }; + + const state = { + sourceBucketName: "bucket-no-lock", + destinationBucketName: "bucket-lock-enabled", + }; + + const action = cleanupAction(Scenarios, mockClient); + + await action.handle(state); + + expect(mockClient.send).toHaveBeenCalledTimes(4); + expect(mockClient.send).toHaveBeenNthCalledWith( + 1, + expect.any(ListObjectVersionsCommand), + ); + expect(mockClient.send).toHaveBeenNthCalledWith( + 3, + expect.any(ListObjectVersionsCommand), + ); + expect(mockClient.send).toHaveBeenNthCalledWith( + 3, + expect.any(ListObjectVersionsCommand), + ); + }); +}); diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/conditional-requests.integration.test.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/conditional-requests.integration.test.js new file mode 100644 index 00000000000..a127c8b9e4c --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/conditional-requests.integration.test.js @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { describe, it, expect, afterAll } from "vitest"; +import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3"; +import { createBucketsAction } from "./setup.steps.js"; +import * as Scenarios from "@aws-doc-sdk-examples/lib/scenario/index.js"; +import { legallyEmptyAndDeleteBuckets } from "../../libs/s3Utils.js"; + +const bucketPrefix = "js-conditional-requests"; +const client = new S3Client({}); + +describe("S3 Object Locking Integration Tests", () => { + const state = { + sourceBucketName: `${bucketPrefix}-no-lock`, + destinationBucketName: `${bucketPrefix}-lock-enabled`, + }; + + afterAll(async () => { + // Clean up resources + const buckets = [state.sourceBucketName, state.destinationBucketName]; + + await legallyEmptyAndDeleteBuckets(buckets); + }); + + it("should create buckets with correct configurations", async () => { + const action = createBucketsAction(Scenarios, client); + await action.handle(state); + + const bucketList = await client.send(new ListBucketsCommand({})); + expect(bucketList.Buckets?.map((bucket) => bucket.Name)).toContain( + state.sourceBucketName, + ); + expect(bucketList.Buckets?.map((bucket) => bucket.Name)).toContain( + state.destinationBucketName, + ); + }); +}); diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/index.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/index.js new file mode 100644 index 00000000000..6ba394378c7 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/index.js @@ -0,0 +1,81 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as Scenarios from "@aws-doc-sdk-examples/lib/scenario/index.js"; +import { + exitOnFalse, + loadState, + saveState, +} from "@aws-doc-sdk-examples/lib/scenario/steps-common.js"; + +import { welcome, welcomeContinue } from "./welcome.steps.js"; +import { + confirmCreateBuckets, + confirmPopulateBuckets, + createBuckets, + createBucketsAction, + getBucketPrefix, + populateBuckets, + populateBucketsAction, +} from "./setup.steps.js"; + +/** + * @param {Scenarios} scenarios + * @param {Record} initialState + */ +export const getWorkflowStages = (scenarios, initialState = {}) => { + const client = new S3Client({}); + + return { + deploy: new scenarios.Scenario( + "S3 Conditional Requests - Deploy", + [ + welcome(scenarios), + welcomeContinue(scenarios), + exitOnFalse(scenarios, "welcomeContinue"), + getBucketPrefix(scenarios), + createBuckets(scenarios), + confirmCreateBuckets(scenarios), + exitOnFalse(scenarios, "confirmCreateBuckets"), + createBucketsAction(scenarios, client), + populateBuckets(scenarios), + confirmPopulateBuckets(scenarios), + exitOnFalse(scenarios, "confirmPopulateBuckets"), + populateBucketsAction(scenarios, client), + saveState, + ], + initialState, + ), + demo: new scenarios.Scenario( + "S3 Conditional Requests - Demo", + [loadState, welcome(scenarios), replAction(scenarios, client)], + initialState, + ), + clean: new scenarios.Scenario( + "S3 Conditional Requests - Destroy", + [ + loadState, + confirmCleanup(scenarios), + exitOnFalse(scenarios, "confirmCleanup"), + cleanupAction(scenarios, client), + ], + initialState, + ), + }; +}; + +// Call function if run directly +import { fileURLToPath } from "node:url"; +import { S3Client } from "@aws-sdk/client-s3"; +import { cleanupAction, confirmCleanup } from "./clean.steps.js"; +import { replAction } from "./repl.steps.js"; + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const objectLockingScenarios = getWorkflowStages(Scenarios); + Scenarios.parseScenarioArgs(objectLockingScenarios, { + name: "Amazon S3 object locking workflow", + description: + "Work with Amazon Simple Storage Service (Amazon S3) object locking features.", + synopsis: + "node index.js --scenario [-h|--help] [-y|--yes] [-v|--verbose]", + }); +} diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/object_name.json b/javascriptv3/example_code/s3/scenarios/conditional-requests/object_name.json new file mode 100644 index 00000000000..4d0d6f5c3ad --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/object_name.json @@ -0,0 +1,3 @@ +{ + "name": "test-111-" +} diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.integration.test.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.integration.test.js new file mode 100644 index 00000000000..42fbcadef61 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.integration.test.js @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { replAction } from "./repl.steps.js"; +import { S3Client } from "@aws-sdk/client-s3"; + +describe("basic scenario", () => { + it( + "should run without error", + async () => { + await replAction({ confirmAll: true }, S3Client); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.js new file mode 100644 index 00000000000..ae76bc2954e --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/repl.steps.js @@ -0,0 +1,439 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +import { + ListObjectVersionsCommand, + GetObjectCommand, + CopyObjectCommand, + PutObjectCommand, +} from "@aws-sdk/client-s3"; +import data from "./object_name.json" assert { type: "json" }; +import { readFile } from "node:fs/promises"; +import { + ScenarioInput, + Scenario, + ScenarioAction, + ScenarioOutput, +} from "../../../libs/scenario/index.js"; + +/** + * @typedef {import("@aws-doc-sdk-examples/lib/scenario/index.js")} Scenarios + */ + +/** + * @typedef {import("@aws-sdk/client-s3").S3Client} S3Client + */ + +const choices = { + EXIT: 0, + LIST_ALL_FILES: 1, + CONDITIONAL_READ: 2, + CONDITIONAL_COPY: 3, + CONDITIONAL_WRITE: 4, +}; + +/** + * @param {Scenarios} scenarios + */ +const replInput = (scenarios) => + new ScenarioInput( + "replChoice", + "Explore the S3 conditional request features by selecting one of the following choices", + { + type: "select", + choices: [ + { name: "Print list of bucket items.", value: choices.LIST_ALL_FILES }, + { + name: "Perform a conditional read.", + value: choices.CONDITIONAL_READ, + }, + { + name: "Perform a conditional copy. These examples use the key name prefix defined in ./object_name.json.", + value: choices.CONDITIONAL_COPY, + }, + { + name: "Perform a conditional write. This example use the sample file ./text02.txt.", + value: choices.CONDITIONAL_WRITE, + }, + { name: "Finish the workflow.", value: choices.EXIT }, + ], + }, + ); + +/** + * @param {S3Client} client + * @param {string[]} buckets + */ +const getAllFiles = async (client, buckets) => { + /** @type {{bucket: string, key: string, version: string}[]} */ + const files = []; + for (const bucket of buckets) { + const objectsResponse = await client.send( + new ListObjectVersionsCommand({ Bucket: bucket }), + ); + for (const version of objectsResponse.Versions || []) { + const { Key } = version; + files.push({ bucket, key: Key }); + } + } + return files; +}; + +/** + * @param {S3Client} client + * @param {string[]} buckets + * @param {string} key + */ +const getEtag = async (client, bucket, key) => { + const objectsResponse = await client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + }), + ); + return objectsResponse.ETag; +}; + +/** + * @param {S3Client} client + * @param {string[]} buckets + */ + +/** + * @param {Scenarios} scenarios + * @param {S3Client} client + */ +export const replAction = (scenarios, client) => + new ScenarioAction( + "replAction", + async (state) => { + const files = await getAllFiles(client, [ + state.sourceBucketName, + state.destinationBucketName, + ]); + + const fileInput = new scenarios.ScenarioInput( + "selectedFile", + "Select a file to use:", + { + type: "select", + choices: files.map((file, index) => ({ + name: `${index + 1}: ${file.bucket}: ${file.key} (Etag: ${ + file.version + })`, + value: index, + })), + }, + ); + const condReadOptions = new scenarios.ScenarioInput( + "selectOption", + "Which conditional read action would you like to take?", + { + type: "select", + choices: [ + "If-Match: using the object's ETag. This condition should succeed.", + "If-None-Match: using the object's ETag. This condition should fail.", + "If-Modified-Since: using yesterday's date. This condition should succeed.", + "If-Unmodified-Since: using yesterday's date. This condition should fail.", + ], + }, + ); + const condCopyOptions = new scenarios.ScenarioInput( + "selectOption", + "Which conditional copy action would you like to take?", + { + type: "select", + choices: [ + "If-Match: using the object's ETag. This condition should succeed.", + "If-None-Match: using the object's ETag. This condition should fail.", + "If-Modified-Since: using yesterday's date. This condition should succeed.", + "If-Unmodified-Since: using yesterday's date. This condition should fail.", + ], + }, + ); + const condWriteOptions = new scenarios.ScenarioInput( + "selectOption", + "Which conditional write action would you like to take?", + { + type: "select", + choices: [ + "IfNoneMatch condition on the object key: If the key is a duplicate, the write will fail.", + ], + }, + ); + + const { replChoice } = state; + + switch (replChoice) { + case choices.LIST_ALL_FILES: { + const files = await getAllFiles(client, [ + state.sourceBucketName, + state.destinationBucketName, + ]); + state.replOutput = files + .map( + (file) => `Items in bucket ${file.bucket}: object: ${file.key} `, + ) + .join("\n"); + break; + } + case choices.CONDITIONAL_READ: + { + const selectedCondRead = await condReadOptions.handle(state); + if ( + selectedCondRead === + "If-Match: using the object's ETag. This condition should succeed." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + const ETag = await getEtag(client, bucket, key); + + try { + await client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + IfMatch: ETag, + }), + ); + state.replOutput = `${key} in bucket ${state.sourceBucketName} read because ETag provided matches the object's ETag.`; + } catch (err) { + state.replOutput = `Unable to read object ${key} in bucket ${state.sourceBucketName}: ${err.message}`; + } + break; + } + if ( + selectedCondRead === + "If-None-Match: using the object's ETag. This condition should fail." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + const ETag = await getEtag(client, bucket, key); + + try { + await client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + IfNoneMatch: ETag, + }), + ); + state.replOutput = `${key} in ${state.sourceBucketName} was returned.`; + } catch (err) { + state.replOutput = `${key} in ${state.sourceBucketName} was not read: ${err.message}`; + } + break; + } + if ( + selectedCondRead === + "If-Modified-Since: using yesterday's date. This condition should succeed." + ) { + const date = new Date(); + date.setDate(date.getDate() - 1); + + const bucket = state.sourceBucketName; + const key = "file01.txt"; + try { + await client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + IfModifiedSince: date, + }), + ); + state.replOutput = `${key} in bucket ${state.sourceBucketName} read because it has been created or modified in the last 24 hours.`; + } catch (err) { + state.replOutput = `Unable to read object ${key} in bucket ${state.sourceBucketName}: ${err.message}`; + } + break; + } + if ( + selectedCondRead === + "If-Unmodified-Since: using yesterday's date. This condition should fail." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + + const date = new Date(); + date.setDate(date.getDate() - 1); + try { + await client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + IfUnmodifiedSince: date, + }), + ); + state.replOutput = `${key} in ${state.sourceBucketName} was read.`; + } catch (err) { + state.replOutput = `${key} in ${state.sourceBucketName} was not read: ${err.message}`; + } + break; + } + } + break; + case choices.CONDITIONAL_COPY: { + const selectedCondCopy = await condCopyOptions.handle(state); + if ( + selectedCondCopy === + "If-Match: using the object's ETag. This condition should succeed." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + const ETag = await getEtag(client, bucket, key); + + const copySource = `${bucket}/${key}`; + // Optionally edit the default key name prefix of the copied object in ./object_name.json. + const name = data.name; + const copiedKey = `${name}${key}`; + try { + await client.send( + new CopyObjectCommand({ + CopySource: copySource, + Bucket: state.destinationBucketName, + Key: copiedKey, + CopySourceIfMatch: ETag, + }), + ); + state.replOutput = `${key} copied as ${copiedKey} to bucket ${state.destinationBucketName} because ETag provided matches the object's ETag.`; + } catch (err) { + state.replOutput = `Unable to copy object ${key} as ${copiedKey} to bucket ${state.destinationBucketName}: ${err.message}`; + } + break; + } + if ( + selectedCondCopy === + "If-None-Match: using the object's ETag. This condition should fail." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + const ETag = await getEtag(client, bucket, key); + const copySource = `${bucket}/${key}`; + // Optionally edit the default key name prefix of the copied object in ./object_name.json. + const name = data.name; + const copiedKey = `${name}${key}`; + + try { + await client.send( + new CopyObjectCommand({ + CopySource: copySource, + Bucket: state.destinationBucketName, + Key: copiedKey, + CopySourceIfNoneMatch: ETag, + }), + ); + state.replOutput = `${copiedKey} copied to bucket ${state.destinationBucketName}`; + } catch (err) { + state.replOutput = `Unable to copy object as ${key} as as ${copiedKey} to bucket ${state.destinationBucketName}: ${err.message}`; + } + break; + } + if ( + selectedCondCopy === + "If-Modified-Since: using yesterday's date. This condition should succeed." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + const copySource = `${bucket}/${key}`; + // Optionally edit the default key name prefix of the copied object in ./object_name.json. + const name = data.name; + const copiedKey = `${name}${key}`; + + const date = new Date(); + date.setDate(date.getDate() - 1); + + try { + await client.send( + new CopyObjectCommand({ + CopySource: copySource, + Bucket: state.destinationBucketName, + Key: copiedKey, + CopySourceIfModifiedSince: date, + }), + ); + state.replOutput = `${key} copied as ${copiedKey} to bucket ${state.destinationBucketName} because it has been created or modified in the last 24 hours.`; + } catch (err) { + state.replOutput = `Unable to copy object ${key} as ${copiedKey} to bucket ${state.destinationBucketName} : ${err.message}`; + } + break; + } + if ( + selectedCondCopy === + "If-Unmodified-Since: using yesterday's date. This condition should fail." + ) { + const bucket = state.sourceBucketName; + const key = "file01.txt"; + const copySource = `${bucket}/${key}`; + // Optionally edit the default key name prefix of the copied object in ./object_name.json. + const name = data.name; + const copiedKey = `${name}${key}`; + + const date = new Date(); + date.setDate(date.getDate() - 1); + + try { + await client.send( + new CopyObjectCommand({ + CopySource: copySource, + Bucket: state.destinationBucketName, + Key: copiedKey, + CopySourceIfUnmodifiedSince: date, + }), + ); + state.replOutput = `${copiedKey} copied to bucket ${state.destinationBucketName} because it has not been created or modified in the last 24 hours.`; + } catch (err) { + state.replOutput = `Unable to copy object ${key} to bucket ${state.destinationBucketName}: ${err.message}`; + } + } + break; + } + case choices.CONDITIONAL_WRITE: + { + const selectedCondWrite = await condWriteOptions.handle(state); + if ( + selectedCondWrite === + "IfNoneMatch condition on the object key: If the key is a duplicate, the write will fail." + ) { + // Optionally edit the default key name prefix of the copied object in ./object_name.json. + const key = "text02.txt"; + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const filePath = path.join(__dirname, "text02.txt"); + try { + await client.send( + new PutObjectCommand({ + Bucket: `${state.destinationBucketName}`, + Key: `${key}`, + Body: await readFile(filePath), + IfNoneMatch: "*", + }), + ); + state.replOutput = `${key} uploaded to bucket ${state.destinationBucketName} because the key is not a duplicate.`; + } catch (err) { + state.replOutput = `Unable to upload object to bucket ${state.destinationBucketName}:${err.message}`; + } + break; + } + } + break; + + default: + throw new Error(`Invalid replChoice: ${replChoice}`); + } + }, + { + whileConfig: { + whileFn: ({ replChoice }) => replChoice !== choices.EXIT, + input: replInput(scenarios), + output: new ScenarioOutput("REPL output", (state) => state.replOutput, { + preformatted: true, + }), + }, + }, + ); + +export { replInput, choices }; diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/setup.steps.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/setup.steps.js new file mode 100644 index 00000000000..0d8d28850e9 --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/setup.steps.js @@ -0,0 +1,146 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + ChecksumAlgorithm, + CreateBucketCommand, + PutObjectCommand, + BucketAlreadyExists, + BucketAlreadyOwnedByYou, + S3ServiceException, + waitUntilBucketExists, +} from "@aws-sdk/client-s3"; + +/** + * @typedef {import("@aws-doc-sdk-examples/lib/scenario/index.js")} Scenarios + */ + +/** + * @typedef {import("@aws-sdk/client-s3").S3Client} S3Client + */ + +/** + * @param {Scenarios} scenarios + */ +const getBucketPrefix = (scenarios) => + new scenarios.ScenarioInput( + "bucketPrefix", + "Provide a prefix that will be used for bucket creation.", + { type: "input", default: "amzn-s3-demo-bucket" }, + ); +/** + * @param {Scenarios} scenarios + */ +const createBuckets = (scenarios) => + new scenarios.ScenarioOutput( + "createBuckets", + (state) => `The following buckets will be created: + ${state.bucketPrefix}-source-bucket. + ${state.bucketPrefix}-destination-bucket.`, + { preformatted: true }, + ); + +/** + * @param {Scenarios} scenarios + */ +const confirmCreateBuckets = (scenarios) => + new scenarios.ScenarioInput("confirmCreateBuckets", "Create the buckets?", { + type: "confirm", + }); + +/** + * @param {Scenarios} scenarios + * @param {S3Client} client + */ +const createBucketsAction = (scenarios, client) => + new scenarios.ScenarioAction("createBucketsAction", async (state) => { + const sourceBucketName = `${state.bucketPrefix}-source-bucket`; + const destinationBucketName = `${state.bucketPrefix}-destination-bucket`; + + try { + await client.send( + new CreateBucketCommand({ + Bucket: sourceBucketName, + }), + ); + await waitUntilBucketExists({ client }, { Bucket: sourceBucketName }); + await client.send( + new CreateBucketCommand({ + Bucket: destinationBucketName, + }), + ); + await waitUntilBucketExists( + { client }, + { Bucket: destinationBucketName }, + ); + + state.sourceBucketName = sourceBucketName; + state.destinationBucketName = destinationBucketName; + } catch (caught) { + if ( + caught instanceof BucketAlreadyExists || + caught instanceof BucketAlreadyOwnedByYou + ) { + console.error(`${caught.name}: ${caught.message}`); + state.earlyExit = true; + } else { + throw caught; + } + } + }); + +/** + * @param {Scenarios} scenarios + */ +const populateBuckets = (scenarios) => + new scenarios.ScenarioOutput( + "populateBuckets", + (state) => `The following test files will be created: + file01.txt in ${state.bucketPrefix}-source-bucket.`, + { preformatted: true }, + ); + +/** + * @param {Scenarios} scenarios + */ +const confirmPopulateBuckets = (scenarios) => + new scenarios.ScenarioInput( + "confirmPopulateBuckets", + "Populate the buckets?", + { type: "confirm" }, + ); + +/** + * @param {Scenarios} scenarios + * @param {S3Client} client + */ +const populateBucketsAction = (scenarios, client) => + new scenarios.ScenarioAction("populateBucketsAction", async (state) => { + try { + await client.send( + new PutObjectCommand({ + Bucket: state.sourceBucketName, + Key: "file01.txt", + Body: "Content", + ChecksumAlgorithm: ChecksumAlgorithm.SHA256, + }), + ); + } catch (caught) { + if (caught instanceof S3ServiceException) { + console.error( + `Error from S3 while uploading object. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } + }); + +export { + confirmCreateBuckets, + confirmPopulateBuckets, + createBuckets, + createBucketsAction, + getBucketPrefix, + populateBuckets, + populateBucketsAction, +}; diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/text02.txt b/javascriptv3/example_code/s3/scenarios/conditional-requests/text02.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/javascriptv3/example_code/s3/scenarios/conditional-requests/welcome.steps.js b/javascriptv3/example_code/s3/scenarios/conditional-requests/welcome.steps.js new file mode 100644 index 00000000000..0ba5b25c7bc --- /dev/null +++ b/javascriptv3/example_code/s3/scenarios/conditional-requests/welcome.steps.js @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +/** + * @typedef {import("@aws-doc-sdk-examples/lib/scenario/index.js")} Scenarios + */ + +/** + * @param {Scenarios} scenarios + */ +const welcome = (scenarios) => + new scenarios.ScenarioOutput( + "welcome", + "This example demonstrates the use of conditional requests for S3 operations." + + " You can use conditional requests to add preconditions to S3 read requests to return " + + "or copy an object based on its Entity tag (ETag), or last modified date.You can use " + + "a conditional write requests to prevent overwrites by ensuring there is no existing " + + "object with the same key.\n" + + "This example will enable you to perform conditional reads and writes that will succeed " + + "or fail based on your selected options.\n" + + "Sample buckets and a sample object will be created as part of the example.\n" + + "Some steps require a key name prefix to be defined by the user. Before you begin, you can " + + "optionally edit this prefix in ./object_name.json. If you do so, please reload the scenario before you begin.", + { header: true }, + ); + +/** + * @param {Scenarios} scenarios + */ +const welcomeContinue = (scenarios) => + new scenarios.ScenarioInput( + "welcomeContinue", + "Press Enter when you are ready to start.", + { type: "confirm" }, + ); + +export { welcome, welcomeContinue }; diff --git a/javascriptv3/example_code/s3/scenarios/object-locking/clean.steps.unit.test.js b/javascriptv3/example_code/s3/scenarios/object-locking/clean.steps.unit.test.js index b68cbef65ea..ab9c1666ff9 100644 --- a/javascriptv3/example_code/s3/scenarios/object-locking/clean.steps.unit.test.js +++ b/javascriptv3/example_code/s3/scenarios/object-locking/clean.steps.unit.test.js @@ -7,7 +7,7 @@ import * as Scenarios from "@aws-doc-sdk-examples/lib/scenario/index.js"; import { cleanupAction } from "./clean.steps.js"; -describe("clean.steps.js", () => { +describe.skip("clean.steps.js", () => { it("should call ListObjectVersionsCommand once for each bucket", async () => { const mockClient = { send: vi diff --git a/javascriptv3/example_code/s3/scenarios/object-locking/index.unit.test.js b/javascriptv3/example_code/s3/scenarios/object-locking/index.unit.test.js index 19dd135c2f4..fc68c26c1b8 100644 --- a/javascriptv3/example_code/s3/scenarios/object-locking/index.unit.test.js +++ b/javascriptv3/example_code/s3/scenarios/object-locking/index.unit.test.js @@ -13,7 +13,7 @@ vi.doMock("fs/promises", () => ({ const { getWorkflowStages } = await import("./index.js"); -describe("S3 Object Locking Workflow", () => { +describe.skip("S3 Object Locking Workflow", () => { /** * @param {{}} state */ diff --git a/javascriptv3/example_code/s3/scenarios/object-locking/repl.steps.unit.test.js b/javascriptv3/example_code/s3/scenarios/object-locking/repl.steps.unit.test.js index c4796bb81a6..6adfb5cffdd 100644 --- a/javascriptv3/example_code/s3/scenarios/object-locking/repl.steps.unit.test.js +++ b/javascriptv3/example_code/s3/scenarios/object-locking/repl.steps.unit.test.js @@ -6,7 +6,7 @@ import * as Scenarios from "@aws-doc-sdk-examples/lib/scenario/index.js"; import { choices, replAction, replInput } from "./repl.steps.js"; import { ChecksumAlgorithm } from "@aws-sdk/client-s3"; -describe("repl.steps.js", () => { +describe.skip("repl.steps.js", () => { const mockClient = { send: vi.fn(), }; @@ -17,7 +17,7 @@ describe("repl.steps.js", () => { retentionBucketName: "bucket-retention", }; - describe("replInput", () => { + describe.skip("replInput", () => { it("should create a ScenarioInput with the correct choices", () => { const input = replInput(Scenarios); expect(input).toBeInstanceOf(Scenarios.ScenarioInput); @@ -28,7 +28,7 @@ describe("repl.steps.js", () => { }); }); - describe("replAction", () => { + describe.skip("replAction", () => { beforeEach(() => { mockClient.send.mockReset(); }); diff --git a/javascriptv3/example_code/s3/scenarios/object-locking/setup.steps.unit.test.js b/javascriptv3/example_code/s3/scenarios/object-locking/setup.steps.unit.test.js index d1960e44e93..914f83bead3 100644 --- a/javascriptv3/example_code/s3/scenarios/object-locking/setup.steps.unit.test.js +++ b/javascriptv3/example_code/s3/scenarios/object-locking/setup.steps.unit.test.js @@ -10,7 +10,7 @@ import { updateLockPolicyAction, } from "./setup.steps.js"; -describe("setup.steps.js", () => { +describe.skip("setup.steps.js", () => { const mockClient = { send: vi.fn(), }; @@ -25,7 +25,7 @@ describe("setup.steps.js", () => { vi.resetAllMocks(); }); - describe("createBucketsAction", () => { + describe.skip("createBucketsAction", () => { it("should create three buckets with the correct configurations", async () => { const action = createBucketsAction(Scenarios, mockClient); await action.handle(state); @@ -56,7 +56,7 @@ describe("setup.steps.js", () => { }); }); - describe("populateBucketsAction", () => { + describe.skip("populateBucketsAction", () => { it("should upload six files to the three buckets", async () => { const action = populateBucketsAction(Scenarios, mockClient); await action.handle(state); @@ -79,7 +79,7 @@ describe("setup.steps.js", () => { }); }); - describe("updateRetentionAction", () => { + describe.skip("updateRetentionAction", () => { it("should enable versioning and set a retention period on the retention bucket", async () => { const action = updateRetentionAction(Scenarios, mockClient); await action.handle(state); @@ -115,7 +115,7 @@ describe("setup.steps.js", () => { }); }); - describe("updateLockPolicyAction", () => { + describe.skip("updateLockPolicyAction", () => { it("should add an object lock policy to the lock-enabled bucket", async () => { const action = updateLockPolicyAction(Scenarios, mockClient); await action.handle(state); diff --git a/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-match.integration.test.js b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-match.integration.test.js new file mode 100644 index 00000000000..7e10f2c04c9 --- /dev/null +++ b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-match.integration.test.js @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/copy-object-conditional-request-if-match.js"; + +describe("test copy-object-conditional-request-if-match", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + sourceBucketName: "amzn-s3-demo-bucket", + sourceKeyName: "mykey", + destinationBucketName: "amzn-s3-demo-bucket1", + eTag: "123456789", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-modified-since.integration.test.js b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-modified-since.integration.test.js new file mode 100644 index 00000000000..e667b96c086 --- /dev/null +++ b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-modified-since.integration.test.js @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/copy-object-conditional-request-if-modified-since.js"; + +describe("test copy-object-conditional-request-if-modified-since", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + sourceBucketName: "amzn-s3-demo-bucket", + sourceKeyName: "mykey", + destinationBucketName: "amzn-s3-demo-bucket1", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-none-match.integration.test.js b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-none-match.integration.test.js new file mode 100644 index 00000000000..429b34f1551 --- /dev/null +++ b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-none-match.integration.test.js @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/copy-object-conditional-request-if-none-match.js"; + +describe("test copy-object-conditional-request-if-none-match", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + sourceBucketName: "amzn-s3-demo-bucket", + sourceKeyName: "mykey", + destinationBucketName: "amzn-s3-demo-bucket1", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-unmodified-since.integration.test.js b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-unmodified-since.integration.test.js new file mode 100644 index 00000000000..ebae222c4bb --- /dev/null +++ b/javascriptv3/example_code/s3/tests/copy-object-conditional-request-if-unmodified-since.integration.test.js @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/copy-object-conditional-request-if-unmodified-since.js"; + +describe("test copy-object-conditional-request-if-unmodified-since", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + sourceBucketName: "amzn-s3-demo-bucket", + sourceKeyName: "mykey", + destinationBucketName: "amzn-s3-demo-bucket1", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-match.integration.test.js b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-match.integration.test.js new file mode 100644 index 00000000000..993f3a42af5 --- /dev/null +++ b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-match.integration.test.js @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/get-object-conditional-request-if-match.js"; + +describe("test get-object-conditional-request-if-match", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + bucketName: "amzn-s3-demo-bucket", + key: "myKey", + eTag: "123456789", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-modified-since.integration.test.js b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-modified-since.integration.test.js new file mode 100644 index 00000000000..30d687a646a --- /dev/null +++ b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-modified-since.integration.test.js @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/get-object-conditional-request-if-modified-since.js"; + +describe("test get-object-conditional-request-if-modified-since", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + bucketName: "amzn-s3-demo-bucket", + key: "myKey", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-none-match.integration.test.js b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-none-match.integration.test.js new file mode 100644 index 00000000000..c886380c2ef --- /dev/null +++ b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-none-match.integration.test.js @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/get-object-conditional-request-if-none-match.js"; + +describe("test get-object-conditional-request-if-none-match", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + bucketName: "amzn-s3-demo-bucket", + key: "myKey", + eTag: "123456789", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-unmodified-since.integration.test.js b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-unmodified-since.integration.test.js new file mode 100644 index 00000000000..f36bf527968 --- /dev/null +++ b/javascriptv3/example_code/s3/tests/get-object-conditional-request-if-unmodified-since.integration.test.js @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/get-object-conditional-request-if-unmodified-since.js"; + +describe("test get-object-conditional-request-if-unmodified-since", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + bucketName: "amzn-s3-demo-bucket", + key: "myKey", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/put-object-conditional-request-if-none-match.integration.test.js b/javascriptv3/example_code/s3/tests/put-object-conditional-request-if-none-match.integration.test.js new file mode 100644 index 00000000000..d6cc3a3165d --- /dev/null +++ b/javascriptv3/example_code/s3/tests/put-object-conditional-request-if-none-match.integration.test.js @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { main } from "../actions/put-object-conditional-request-if-none-match.js"; + +describe("test put-object-conditional-request-if-none-match", () => { + it( + "should not re-throw service exceptions", + async () => { + await main({ + destinationBucketName: "amzn-s3-demo-bucket1", + }); + }, + { timeout: 600000 }, + ); +}); diff --git a/javascriptv3/example_code/s3/tests/text01.txt b/javascriptv3/example_code/s3/tests/text01.txt new file mode 100644 index 00000000000..11e519d1129 --- /dev/null +++ b/javascriptv3/example_code/s3/tests/text01.txt @@ -0,0 +1 @@ +This is a sample text file for use in some action examples in this folder. \ No newline at end of file diff --git a/javascriptv3/example_code/s3/text01.txt b/javascriptv3/example_code/s3/text01.txt new file mode 100644 index 00000000000..11e519d1129 --- /dev/null +++ b/javascriptv3/example_code/s3/text01.txt @@ -0,0 +1 @@ +This is a sample text file for use in some action examples in this folder. \ No newline at end of file diff --git a/javascriptv3/example_code/secrets-manager/package.json b/javascriptv3/example_code/secrets-manager/package.json index b211450f110..d3cb01ddef2 100644 --- a/javascriptv3/example_code/secrets-manager/package.json +++ b/javascriptv3/example_code/secrets-manager/package.json @@ -7,7 +7,7 @@ "@aws-sdk/client-secrets-manager": "^3.386.0" }, "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/secrets-manager-test-results.junit.xml" }, "type": "module", "devDependencies": { diff --git a/javascriptv3/example_code/ses/package.json b/javascriptv3/example_code/ses/package.json index 644ee0b9be0..9f08942d8d9 100644 --- a/javascriptv3/example_code/ses/package.json +++ b/javascriptv3/example_code/ses/package.json @@ -5,7 +5,7 @@ "license": "Apache 2.0", "type": "module", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/ses-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/sfn/package.json b/javascriptv3/example_code/sfn/package.json index 42bd9a9d4e9..c6926798200 100644 --- a/javascriptv3/example_code/sfn/package.json +++ b/javascriptv3/example_code/sfn/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "author": "Corey Pyle ", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "license": "Apache-2.0", "type": "module", diff --git a/javascriptv3/example_code/sns/package.json b/javascriptv3/example_code/sns/package.json index eb1ad24fbe4..cc1c6d8ee06 100644 --- a/javascriptv3/example_code/sns/package.json +++ b/javascriptv3/example_code/sns/package.json @@ -7,7 +7,7 @@ "@aws-sdk/client-sns": "^3.370.0" }, "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/sns-test-results.junit.xml" }, "type": "module", "devDependencies": { diff --git a/javascriptv3/example_code/sqs/package.json b/javascriptv3/example_code/sqs/package.json index 8604ab6d006..7728434b84f 100644 --- a/javascriptv3/example_code/sqs/package.json +++ b/javascriptv3/example_code/sqs/package.json @@ -5,7 +5,7 @@ "type": "module", "license": "Apache-2.0", "scripts": { - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/sqs-test-results.junit.xml" }, "dependencies": { "@aws-doc-sdk-examples/lib": "^1.0.0", diff --git a/javascriptv3/example_code/ssm/package.json b/javascriptv3/example_code/ssm/package.json index 18c56b56074..e50d59f5777 100644 --- a/javascriptv3/example_code/ssm/package.json +++ b/javascriptv3/example_code/ssm/package.json @@ -6,8 +6,8 @@ "test": "tests" }, "scripts": { - "test": "vitest run **/*.unit.test.js", - "integration-test": "vitest run **/*.integration.test.js --reporter=junit --outputFile=test_results/$npm_package_name.junit.xml" + "test": "vitest run unit", + "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/ssm-test-results.junit.xml" }, "author": "beqqrry@amazon.com", "license": "ISC", diff --git a/javascriptv3/example_code/sts/package.json b/javascriptv3/example_code/sts/package.json index 6bd25f31b21..56ad3ed3a74 100644 --- a/javascriptv3/example_code/sts/package.json +++ b/javascriptv3/example_code/sts/package.json @@ -4,7 +4,7 @@ "author": "Corey Pyle ", "license": "Apache-2.0", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "dependencies": { "@aws-sdk/client-sts": "^3.254.0" diff --git a/javascriptv3/example_code/support/package.json b/javascriptv3/example_code/support/package.json index 3a12ffbac7a..e50b3c07b69 100644 --- a/javascriptv3/example_code/support/package.json +++ b/javascriptv3/example_code/support/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Examples demonstrating how to use the AWS SDK for JavaScript (v3) to interact with AWS Support.", "scripts": { - "test": "vitest run **/*.unit.test.js" + "test": "vitest run unit" }, "author": "corepyle@amazon.com", "license": "Apache-2.0", From 3ae0dd7e193cb226d6ab4d0577bbc90be3fd62d2 Mon Sep 17 00:00:00 2001 From: Nicholas Doropoulos <13554823+nick22d@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:46:29 +0100 Subject: [PATCH 051/144] Correct the typos found in the comments (#7245) --- aws-cli/bash-linux/iam/iam_operations.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aws-cli/bash-linux/iam/iam_operations.sh b/aws-cli/bash-linux/iam/iam_operations.sh index 0b57cd9ec86..ac375b1c00c 100644 --- a/aws-cli/bash-linux/iam/iam_operations.sh +++ b/aws-cli/bash-linux/iam/iam_operations.sh @@ -133,7 +133,7 @@ function iam_create_user() { # bashsupport disable=BP5008 function usage() { echo "function iam_create_user" - echo "Creates an WS Identity and Access Management (IAM) user. You must supply a username:" + echo "Creates an AWS Identity and Access Management (IAM) user. You must supply a username:" echo " -u user_name The name of the user. It must be unique within the account." echo "" } @@ -663,7 +663,7 @@ function iam_delete_policy() { # bashsupport disable=BP5008 function usage() { echo "function iam_delete_policy" - echo "Deletes an WS Identity and Access Management (IAM) policy" + echo "Deletes an AWS Identity and Access Management (IAM) policy" echo " -n policy_arn -- The name of the IAM policy arn." echo "" } @@ -733,7 +733,7 @@ function iam_delete_role() { # bashsupport disable=BP5008 function usage() { echo "function iam_delete_role" - echo "Deletes an WS Identity and Access Management (IAM) role" + echo "Deletes an AWS Identity and Access Management (IAM) role" echo " -n role_name -- The name of the IAM role." echo "" } @@ -922,7 +922,7 @@ function iam_delete_access_key() { # bashsupport disable=BP5008 function usage() { echo "function iam_delete_access_key" - echo "Deletes an WS Identity and Access Management (IAM) access key for the specified IAM user" + echo "Deletes an AWS Identity and Access Management (IAM) access key for the specified IAM user" echo " -u user_name The name of the user." echo " -k access_key The access key to delete." echo "" @@ -1002,7 +1002,7 @@ function iam_delete_user() { # bashsupport disable=BP5008 function usage() { echo "function iam_delete_user" - echo "Deletes an WS Identity and Access Management (IAM) user. You must supply a username:" + echo "Deletes an AWS Identity and Access Management (IAM) user. You must supply a username:" echo " -u user_name The name of the user." echo "" } From e674b454a6c3de8f08c565b20b87804e2606417a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 12:18:00 -0600 Subject: [PATCH 052/144] Bump net-imap from 0.3.4 to 0.3.8 in /ruby (#7237) Bumps [net-imap](https://github.com/ruby/net-imap) from 0.3.4 to 0.3.8. - [Release notes](https://github.com/ruby/net-imap/releases) - [Commits](https://github.com/ruby/net-imap/compare/v0.3.4...v0.3.8) --- updated-dependencies: - dependency-name: net-imap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ruby/Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index a69e07c85df..0f42d6feb0d 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -1399,7 +1399,7 @@ GEM base64 (0.2.0) cli-ui (2.2.3) concurrent-ruby (1.2.2) - date (3.3.3) + date (3.4.1) diff-lcs (1.5.0) i18n (1.14.1) concurrent-ruby (~> 1.0) @@ -1417,12 +1417,12 @@ GEM multi_json (1.15.0) mustermann (3.0.3) ruby2_keywords (~> 0.0.1) - net-imap (0.3.4) + net-imap (0.3.8) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout net-smtp (0.3.3) net-protocol @@ -1503,7 +1503,7 @@ GEM tilt (2.4.0) time (0.2.2) date - timeout (0.3.2) + timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) From 6193cfa9a8eb0b9215a00c6531559548daab7301 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:23:21 +0000 Subject: [PATCH 053/144] Bump rack, rspec, rubocop, rubocop-rake and sinatra in /ruby (#7248) Bumps [rack](https://github.com/rack/rack), [rspec](https://github.com/rspec/rspec-metagem), [rubocop](https://github.com/rubocop/rubocop), [rubocop-rake](https://github.com/rubocop/rubocop-rake) and [sinatra](https://github.com/sinatra/sinatra). These dependencies needed to be updated together. Updates `rack` from 3.1.8 to 3.1.10 - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.1.8...v3.1.10) Updates `rspec` from 3.12.0 to 3.13.0 - [Commits](https://github.com/rspec/rspec-metagem/compare/v3.12.0...v3.13.0) Updates `rubocop` from 1.52.0 to 1.72.2 - [Release notes](https://github.com/rubocop/rubocop/releases) - [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop/compare/v1.52.0...v1.72.2) Updates `rubocop-rake` from 0.6.0 to 0.7.1 - [Release notes](https://github.com/rubocop/rubocop-rake/releases) - [Changelog](https://github.com/rubocop/rubocop-rake/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop-rake/compare/v0.6.0...v0.7.1) Updates `sinatra` from 4.1.0 to 4.1.1 - [Changelog](https://github.com/sinatra/sinatra/blob/main/CHANGELOG.md) - [Commits](https://github.com/sinatra/sinatra/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: rack dependency-type: direct:production - dependency-name: rspec dependency-type: direct:production - dependency-name: rubocop dependency-type: direct:production - dependency-name: rubocop-rake dependency-type: direct:production - dependency-name: sinatra dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ruby/Gemfile.lock | 76 ++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 0f42d6feb0d..923f7fe9eef 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -1400,12 +1400,14 @@ GEM cli-ui (2.2.3) concurrent-ruby (1.2.2) date (3.4.1) - diff-lcs (1.5.0) + diff-lcs (1.6.0) i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.6.3) - logger (1.6.1) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + logger (1.6.6) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -1431,50 +1433,53 @@ GEM time uri openssl (3.1.0) - parallel (1.23.0) - parser (3.2.2.1) + parallel (1.26.3) + parser (3.3.7.1) ast (~> 2.4.1) + racc pp (0.4.0) prettyprint prettyprint (0.1.1) - rack (3.1.8) - rack-protection (4.1.0) + racc (1.8.1) + rack (3.1.10) + rack-protection (4.1.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) - rack-session (2.0.0) + rack-session (2.1.0) + base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.8.0) - rexml (3.3.9) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + regexp_parser (2.10.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.0) - rubocop (1.52.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + rubocop (1.72.2) json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.0) + parser (>= 3.3.1.0) rubocop-github (0.20.0) rubocop (>= 1.37) rubocop-performance (>= 1.15) @@ -1486,27 +1491,30 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rake (0.6.0) - rubocop (~> 1.0) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) sequel (5.69.0) - sinatra (4.1.0) + sinatra (4.1.1) logger (>= 1.6.0) mustermann (~> 3.0) rack (>= 3.0.0, < 4) - rack-protection (= 4.1.0) + rack-protection (= 4.1.1) rack-session (>= 2.0.0, < 3) tilt (~> 2.0) stringio (3.0.7) - tilt (2.4.0) + tilt (2.6.0) time (0.2.2) date timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.4.2) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) uri (0.12.2) zip (2.0.2) From f75c55816623cf0cc029699849dc18e3fc84c8fe Mon Sep 17 00:00:00 2001 From: Laren-AWS <57545972+Laren-AWS@users.noreply.github.com> Date: Mon, 17 Feb 2025 12:13:05 -0800 Subject: [PATCH 054/144] Tools: Update to tools release 2025.07.0 (#7249) Update to tools release 2025.07.0 --- .github/workflows/validate-doc-metadata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-doc-metadata.yml b/.github/workflows/validate-doc-metadata.yml index c048e1aa3e2..28caa15e53a 100644 --- a/.github/workflows/validate-doc-metadata.yml +++ b/.github/workflows/validate-doc-metadata.yml @@ -16,7 +16,7 @@ jobs: - name: checkout repo content uses: actions/checkout@v4 - name: validate metadata - uses: awsdocs/aws-doc-sdk-examples-tools@2025.05.1 + uses: awsdocs/aws-doc-sdk-examples-tools@2025.07.0 with: doc_gen_only: "False" strict_titles: "True" From a771a94b82d0d46aa10e2c49087d873b9a0bfd6b Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 18 Feb 2025 11:36:12 -0500 Subject: [PATCH 055/144] updated POM --- javav2/example_code/entityresolution/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index 0e9154ddd6f..d2fee1f06c4 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -7,7 +7,6 @@ org.example entityresolution 1.0-SNAPSHOT - UTF-8 17 From b214fb619c19d1412d2b1ae5a63ba9b6524c2b78 Mon Sep 17 00:00:00 2001 From: Scott Macdonald <57190223+scmacdon@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:10:32 -0500 Subject: [PATCH 056/144] Java/Kotlin made some minor modifications to SDK examples (#7228) --- .../forecast/src/test/java/ForecastTest.java | 23 -- .../java/com/example/s3/PutBucketLogging.java | 15 +- .../java/com/example/sns/CreateFIFOTopic.java | 4 +- javav2/usecases/topics_and_queues/pom.xml | 14 +- .../java/com/example/sns/SNSWorkflow.java | 286 +++++++++--------- .../src/test/java/AWSSNSTest.java | 4 +- .../com/example/ecr/scenario/ECRScenario.kt | 12 +- .../com/kotlin/translate/BatchTranslation.kt | 2 +- .../src/test/kotlin/AWSSNSTest.kt | 2 +- 9 files changed, 177 insertions(+), 185 deletions(-) diff --git a/javav2/example_code/forecast/src/test/java/ForecastTest.java b/javav2/example_code/forecast/src/test/java/ForecastTest.java index 81e86702d39..af6bac61cce 100644 --- a/javav2/example_code/forecast/src/test/java/ForecastTest.java +++ b/javav2/example_code/forecast/src/test/java/ForecastTest.java @@ -45,29 +45,6 @@ public static void setUp() { predARN = values.getPredARN(); forecastName = values.getForecastName() + randomNum; dataSet = values.getDataSet() + randomNum; - - // Uncomment this code block if you prefer using a config.properties file to - // retrieve AWS values required for these tests. - /* - * - * try (InputStream input = - * ForecastTest.class.getClassLoader().getResourceAsStream("config.properties")) - * { - * Properties prop = new Properties(); - * if (input == null) { - * System.out.println("Sorry, unable to find config.properties"); - * return; - * } - * - * // Populate the data members required for all tests. - * predARN = "arn:aws:forecast:us-west-2:814548047983:predictor/ScottPredictor"; - * forecastName = "forecast"+randomNum; - * dataSet = "dataSet"+randomNum; - * - * } catch (IOException ex) { - * ex.printStackTrace(); - * } - */ } @Test diff --git a/javav2/example_code/s3/src/main/java/com/example/s3/PutBucketLogging.java b/javav2/example_code/s3/src/main/java/com/example/s3/PutBucketLogging.java index 6ae08c33c9f..631ea35563d 100644 --- a/javav2/example_code/s3/src/main/java/com/example/s3/PutBucketLogging.java +++ b/javav2/example_code/s3/src/main/java/com/example/s3/PutBucketLogging.java @@ -33,26 +33,28 @@ public static void main(String[] args) { final String usage = """ Usage: - \s + \s Where: bucketName - The Amazon S3 bucket to upload an object into. - targetBucket - The target bucket . + targetBucket - The target bucket. + accountId - The account id. """; - if (args.length != 2) { + if (args.length != 3) { System.out.println(usage); System.exit(1); } String bucketName = args[0]; String targetBucket = args[1]; + String accountId = args[2]; Region region = Region.US_EAST_1; S3Client s3 = S3Client.builder() .region(region) .build(); - setlogRequest(s3, bucketName, targetBucket); + setlogRequest(s3, bucketName, targetBucket, accountId); s3.close(); } @@ -62,10 +64,11 @@ public static void main(String[] args) { * @param s3 an instance of the {@link S3Client} used to interact with the S3 service * @param bucketName the name of the bucket for which logging needs to be enabled * @param targetBucket the name of the target bucket where the logs will be stored + * @param accountId the account Id * * @throws S3Exception if an error occurs while enabling logging for the bucket */ - public static void setlogRequest(S3Client s3, String bucketName, String targetBucket) { + public static void setlogRequest(S3Client s3, String bucketName, String targetBucket, String accountId) { try { GetBucketAclRequest aclRequest = GetBucketAclRequest.builder() .bucket(targetBucket) @@ -96,7 +99,7 @@ public static void setlogRequest(S3Client s3, String bucketName, String targetBu PutBucketLoggingRequest loggingRequest = PutBucketLoggingRequest.builder() .bucket(bucketName) - .expectedBucketOwner("814548047983") + .expectedBucketOwner(accountId) .bucketLoggingStatus(loggingStatus) .build(); diff --git a/javav2/example_code/sns/src/main/java/com/example/sns/CreateFIFOTopic.java b/javav2/example_code/sns/src/main/java/com/example/sns/CreateFIFOTopic.java index 7e32a2fdb05..c0d2aacd0e7 100644 --- a/javav2/example_code/sns/src/main/java/com/example/sns/CreateFIFOTopic.java +++ b/javav2/example_code/sns/src/main/java/com/example/sns/CreateFIFOTopic.java @@ -35,8 +35,8 @@ public static void main(String[] args) { System.exit(1); } - String fifoTopicName = "PriceUpdatesTopic3.fifo"; - String fifoQueueARN = "arn:aws:sqs:us-east-1:814548047983:MyPriceSQS.fifo"; + String fifoTopicName = args[0]; + String fifoQueueARN = args[1]; SnsClient snsClient = SnsClient.builder() .region(Region.US_EAST_1) .build(); diff --git a/javav2/usecases/topics_and_queues/pom.xml b/javav2/usecases/topics_and_queues/pom.xml index f102ba5b2a8..3bf6ab59b1c 100644 --- a/javav2/usecases/topics_and_queues/pom.xml +++ b/javav2/usecases/topics_and_queues/pom.xml @@ -28,8 +28,8 @@ maven-compiler-plugin 3.1 - 8 - 8 + 15 + 15 @@ -39,7 +39,7 @@ software.amazon.awssdk bom - 2.21.20 + 2.29.45 pom import @@ -87,5 +87,13 @@ gson 2.10.1
+ + software.amazon.awssdk + sso + + + software.amazon.awssdk + ssooidc + diff --git a/javav2/usecases/topics_and_queues/src/main/java/com/example/sns/SNSWorkflow.java b/javav2/usecases/topics_and_queues/src/main/java/com/example/sns/SNSWorkflow.java index dd18c3b9809..9ed408659d9 100644 --- a/javav2/usecases/topics_and_queues/src/main/java/com/example/sns/SNSWorkflow.java +++ b/javav2/usecases/topics_and_queues/src/main/java/com/example/sns/SNSWorkflow.java @@ -34,11 +34,13 @@ import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; import software.amazon.awssdk.services.sqs.model.SetQueueAttributesRequest; import software.amazon.awssdk.services.sqs.model.SqsException; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; + import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -47,13 +49,13 @@ /** * Before running this Java V2 code example, set up your development * environment, including your credentials. - * + *

* For more information, see the following documentation topic: - * + *

* https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html - * + *

* This Java example performs these tasks: - * + *

* 1. Gives the user three options to choose from. * 2. Creates an Amazon Simple Notification Service (Amazon SNS) topic. * 3. Creates an Amazon Simple Queue Service (Amazon SQS) queue. @@ -71,28 +73,28 @@ public class SNSWorkflow { public static void main(String[] args) { final String usage = "\n" + - "Usage:\n" + - " \n\n" + - "Where:\n" + - " accountId - Your AWS account Id value."; + "Usage:\n" + + " \n\n" + + "Where:\n" + + " accountId - Your AWS account Id value."; - // if (args.length != 1) { - // System.out.println(usage); - // System.exit(1); - // } + if (args.length != 1) { + System.out.println(usage); + System.exit(1); + } SnsClient snsClient = SnsClient.builder() - .region(Region.US_EAST_1) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .build(); + .region(Region.US_EAST_1) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .build(); SqsClient sqsClient = SqsClient.builder() - .region(Region.US_EAST_1) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .build(); + .region(Region.US_EAST_1) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .build(); Scanner in = new Scanner(System.in); - String accountId = "814548047983"; + String accountId = args[0]; String useFIFO; String duplication = "n"; String topicName; @@ -114,28 +116,28 @@ public static void main(String[] args) { System.out.println(DASHES); System.out.println("Welcome to messaging with topics and queues."); System.out.println("In this scenario, you will create an SNS topic and subscribe an SQS queue to the topic.\n" + - "You can select from several options for configuring the topic and the subscriptions for the queue.\n" + - "You can then post to the topic and see the results in the queue."); + "You can select from several options for configuring the topic and the subscriptions for the queue.\n" + + "You can then post to the topic and see the results in the queue."); System.out.println(DASHES); System.out.println(DASHES); System.out.println("SNS topics can be configured as FIFO (First-In-First-Out).\n" + - "FIFO topics deliver messages in order and support deduplication and message filtering.\n" + - "Would you like to work with FIFO topics? (y/n)"); + "FIFO topics deliver messages in order and support deduplication and message filtering.\n" + + "Would you like to work with FIFO topics? (y/n)"); useFIFO = in.nextLine(); if (useFIFO.compareTo("y") == 0) { selectFIFO = true; System.out.println("You have selected FIFO"); System.out.println(" Because you have chosen a FIFO topic, deduplication is supported.\n" + - " Deduplication IDs are either set in the message or automatically generated from content using a hash function.\n" - + - " If a message is successfully published to an SNS FIFO topic, any message published and determined to have the same deduplication ID,\n" - + - " within the five-minute deduplication interval, is accepted but not delivered.\n" + - " For more information about deduplication, see https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html."); + " Deduplication IDs are either set in the message or automatically generated from content using a hash function.\n" + + + " If a message is successfully published to an SNS FIFO topic, any message published and determined to have the same deduplication ID,\n" + + + " within the five-minute deduplication interval, is accepted but not delivered.\n" + + " For more information about deduplication, see https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html."); System.out.println( - "Would you like to use content-based deduplication instead of entering a deduplication ID? (y/n)"); + "Would you like to use content-based deduplication instead of entering a deduplication ID? (y/n)"); duplication = in.nextLine(); if (duplication.compareTo("y") == 0) { System.out.println("Please enter a group id value"); @@ -191,23 +193,25 @@ public static void main(String[] args) { // Define the policy to use. Make sure that you change the REGION if you are // running this code // in a different region. - String policy = "{\n" + - " \"Statement\": [\n" + - " {\n" + - " \"Effect\": \"Allow\",\n" + - " \"Principal\": {\n" + - " \"Service\": \"sns.amazonaws.com\"\n" + - " },\n" + - " \"Action\": \"sqs:SendMessage\",\n" + - " \"Resource\": \"arn:aws:sqs:us-east-1:" + accountId + ":" + sqsQueueName + "\",\n" + - " \"Condition\": {\n" + - " \"ArnEquals\": {\n" + - " \"aws:SourceArn\": \"arn:aws:sns:us-east-1:" + accountId + ":" + topicName + "\"\n" + - " }\n" + - " }\n" + - " }\n" + - " ]\n" + - " }"; + String policy = """ + { + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Action": "sqs:SendMessage", + "Resource": "arn:aws:sqs:us-east-1:%s:%s", + "Condition": { + "ArnEquals": { + "aws:SourceArn": "arn:aws:sns:us-east-1:%s:%s" + } + } + } + ] + } + """.formatted(accountId, sqsQueueName, accountId, topicName); setQueueAttr(sqsClient, sqsQueueUrl, policy); System.out.println(DASHES); @@ -216,13 +220,13 @@ public static void main(String[] args) { System.out.println("6. Subscribe to the SQS queue."); if (selectFIFO) { System.out.println( - "If you add a filter to this subscription, then only the filtered messages will be received in the queue.\n" - + - "For information about message filtering, see https://docs.aws.amazon.com/sns/latest/dg/sns-message-filtering.html\n" - + - "For this example, you can filter messages by a \"tone\" attribute."); + "If you add a filter to this subscription, then only the filtered messages will be received in the queue.\n" + + + "For information about message filtering, see https://docs.aws.amazon.com/sns/latest/dg/sns-message-filtering.html\n" + + + "For this example, you can filter messages by a \"tone\" attribute."); System.out.println("Would you like to filter messages for " + sqsQueueName + "'s subscription to the topic " - + topicName + "? (y/n)"); + + topicName + "? (y/n)"); String filterAns = in.nextLine(); if (filterAns.compareTo("y") == 0) { boolean moreAns = false; @@ -334,8 +338,8 @@ public static void main(String[] args) { public static void deleteSNSTopic(SnsClient snsClient, String topicArn) { try { DeleteTopicRequest request = DeleteTopicRequest.builder() - .topicArn(topicArn) - .build(); + .topicArn(topicArn) + .build(); DeleteTopicResponse result = snsClient.deleteTopic(request); System.out.println("Status was " + result.sdkHttpResponse().statusCode()); @@ -349,13 +353,13 @@ public static void deleteSNSTopic(SnsClient snsClient, String topicArn) { public static void deleteSQSQueue(SqsClient sqsClient, String queueName) { try { GetQueueUrlRequest getQueueRequest = GetQueueUrlRequest.builder() - .queueName(queueName) - .build(); + .queueName(queueName) + .build(); String queueUrl = sqsClient.getQueueUrl(getQueueRequest).queueUrl(); DeleteQueueRequest deleteQueueRequest = DeleteQueueRequest.builder() - .queueUrl(queueUrl) - .build(); + .queueUrl(queueUrl) + .build(); sqsClient.deleteQueue(deleteQueueRequest); System.out.println(queueName + " was successfully deleted."); @@ -369,12 +373,12 @@ public static void deleteSQSQueue(SqsClient sqsClient, String queueName) { public static void unSub(SnsClient snsClient, String subscriptionArn) { try { UnsubscribeRequest request = UnsubscribeRequest.builder() - .subscriptionArn(subscriptionArn) - .build(); + .subscriptionArn(subscriptionArn) + .build(); UnsubscribeResponse result = snsClient.unsubscribe(request); System.out.println("Status was " + result.sdkHttpResponse().statusCode() - + "\nSubscription was removed for " + request.subscriptionArn()); + + "\nSubscription was removed for " + request.subscriptionArn()); } catch (SnsException e) { System.err.println(e.awsErrorDetails().errorMessage()); @@ -388,16 +392,16 @@ public static void deleteMessages(SqsClient sqsClient, String queueUrl, List entries = new ArrayList<>(); for (Message msg : messages) { DeleteMessageBatchRequestEntry entry = DeleteMessageBatchRequestEntry.builder() - .id(msg.messageId()) - .build(); + .id(msg.messageId()) + .build(); entries.add(entry); } DeleteMessageBatchRequest deleteMessageBatchRequest = DeleteMessageBatchRequest.builder() - .queueUrl(queueUrl) - .entries(entries) - .build(); + .queueUrl(queueUrl) + .entries(entries) + .build(); sqsClient.deleteMessageBatch(deleteMessageBatchRequest); System.out.println("The batch delete of messages was successful"); @@ -413,17 +417,17 @@ public static List receiveMessages(SqsClient sqsClient, String queueUrl try { if (msgAttValue.isEmpty()) { ReceiveMessageRequest receiveMessageRequest = ReceiveMessageRequest.builder() - .queueUrl(queueUrl) - .maxNumberOfMessages(5) - .build(); + .queueUrl(queueUrl) + .maxNumberOfMessages(5) + .build(); return sqsClient.receiveMessage(receiveMessageRequest).messages(); } else { // We know there are filters on the message. ReceiveMessageRequest receiveRequest = ReceiveMessageRequest.builder() - .queueUrl(queueUrl) - .messageAttributeNames(msgAttValue) // Include other message attributes if needed. - .maxNumberOfMessages(5) - .build(); + .queueUrl(queueUrl) + .messageAttributeNames(msgAttValue) // Include other message attributes if needed. + .maxNumberOfMessages(5) + .build(); return sqsClient.receiveMessage(receiveRequest).messages(); } @@ -438,13 +442,13 @@ public static List receiveMessages(SqsClient sqsClient, String queueUrl public static void pubMessage(SnsClient snsClient, String message, String topicArn) { try { PublishRequest request = PublishRequest.builder() - .message(message) - .topicArn(topicArn) - .build(); + .message(message) + .topicArn(topicArn) + .build(); PublishResponse result = snsClient.publish(request); System.out - .println(result.messageId() + " Message sent. Status is " + result.sdkHttpResponse().statusCode()); + .println(result.messageId() + " Message sent. Status is " + result.sdkHttpResponse().statusCode()); } catch (SnsException e) { System.err.println(e.awsErrorDetails().errorMessage()); @@ -453,12 +457,12 @@ public static void pubMessage(SnsClient snsClient, String message, String topicA } public static void pubMessageFIFO(SnsClient snsClient, - String message, - String topicArn, - String msgAttValue, - String duplication, - String groupId, - String deduplicationID) { + String message, + String topicArn, + String msgAttValue, + String duplication, + String groupId, + String deduplicationID) { try { PublishRequest request; @@ -466,48 +470,48 @@ public static void pubMessageFIFO(SnsClient snsClient, if (msgAttValue.isEmpty()) { if (duplication.compareTo("y") == 0) { request = PublishRequest.builder() - .message(message) - .messageGroupId(groupId) - .topicArn(topicArn) - .build(); + .message(message) + .messageGroupId(groupId) + .topicArn(topicArn) + .build(); } else { request = PublishRequest.builder() - .message(message) - .messageDeduplicationId(deduplicationID) - .messageGroupId(groupId) - .topicArn(topicArn) - .build(); + .message(message) + .messageDeduplicationId(deduplicationID) + .messageGroupId(groupId) + .topicArn(topicArn) + .build(); } } else { Map messageAttributes = new HashMap<>(); messageAttributes.put(msgAttValue, MessageAttributeValue.builder() - .dataType("String") - .stringValue("true") - .build()); + .dataType("String") + .stringValue("true") + .build()); if (duplication.compareTo("y") == 0) { request = PublishRequest.builder() - .message(message) - .messageGroupId(groupId) - .topicArn(topicArn) - .build(); + .message(message) + .messageGroupId(groupId) + .topicArn(topicArn) + .build(); } else { // Create a publish request with the message and attributes. request = PublishRequest.builder() - .topicArn(topicArn) - .message(message) - .messageDeduplicationId(deduplicationID) - .messageGroupId(groupId) - .messageAttributes(messageAttributes) - .build(); + .topicArn(topicArn) + .message(message) + .messageDeduplicationId(deduplicationID) + .messageGroupId(groupId) + .messageAttributes(messageAttributes) + .build(); } } // Publish the message to the topic. PublishResponse result = snsClient.publish(request); System.out - .println(result.messageId() + " Message sent. Status was " + result.sdkHttpResponse().statusCode()); + .println(result.messageId() + " Message sent. Status was " + result.sdkHttpResponse().statusCode()); } catch (SnsException e) { System.err.println(e.awsErrorDetails().errorMessage()); @@ -522,27 +526,27 @@ public static String subQueue(SnsClient snsClient, String topicArn, String queue if (filterList.isEmpty()) { // No filter subscription is added. request = SubscribeRequest.builder() - .protocol("sqs") - .endpoint(queueArn) - .returnSubscriptionArn(true) - .topicArn(topicArn) - .build(); + .protocol("sqs") + .endpoint(queueArn) + .returnSubscriptionArn(true) + .topicArn(topicArn) + .build(); SubscribeResponse result = snsClient.subscribe(request); System.out.println("The queue " + queueArn + " has been subscribed to the topic " + topicArn + "\n" + - "with the subscription ARN " + result.subscriptionArn()); + "with the subscription ARN " + result.subscriptionArn()); return result.subscriptionArn(); } else { request = SubscribeRequest.builder() - .protocol("sqs") - .endpoint(queueArn) - .returnSubscriptionArn(true) - .topicArn(topicArn) - .build(); + .protocol("sqs") + .endpoint(queueArn) + .returnSubscriptionArn(true) + .topicArn(topicArn) + .build(); SubscribeResponse result = snsClient.subscribe(request); System.out.println("The queue " + queueArn + " has been subscribed to the topic " + topicArn + "\n" + - "with the subscription ARN " + result.subscriptionArn()); + "with the subscription ARN " + result.subscriptionArn()); String attributeName = "FilterPolicy"; Gson gson = new Gson(); @@ -556,10 +560,10 @@ public static String subQueue(SnsClient snsClient, String topicArn, String queue String updatedJsonString = gson.toJson(jsonObject); System.out.println(updatedJsonString); SetSubscriptionAttributesRequest attRequest = SetSubscriptionAttributesRequest.builder() - .subscriptionArn(result.subscriptionArn()) - .attributeName(attributeName) - .attributeValue(updatedJsonString) - .build(); + .subscriptionArn(result.subscriptionArn()) + .attributeName(attributeName) + .attributeValue(updatedJsonString) + .build(); snsClient.setSubscriptionAttributes(attRequest); return result.subscriptionArn(); @@ -580,9 +584,9 @@ public static void setQueueAttr(SqsClient sqsClient, String queueUrl, String pol attrMap.put(QueueAttributeName.POLICY, policy); SetQueueAttributesRequest attributesRequest = SetQueueAttributesRequest.builder() - .queueUrl(queueUrl) - .attributes(attrMap) - .build(); + .queueUrl(queueUrl) + .attributes(attrMap) + .build(); sqsClient.setQueueAttributes(attributesRequest); System.out.println("The policy has been successfully attached."); @@ -600,9 +604,9 @@ public static String getSQSQueueAttrs(SqsClient sqsClient, String queueUrl) { atts.add(QueueAttributeName.QUEUE_ARN); GetQueueAttributesRequest attributesRequest = GetQueueAttributesRequest.builder() - .queueUrl(queueUrl) - .attributeNames(atts) - .build(); + .queueUrl(queueUrl) + .attributeNames(atts) + .build(); GetQueueAttributesResponse response = sqsClient.getQueueAttributes(attributesRequest); Map queueAtts = response.attributesAsStrings(); @@ -619,24 +623,24 @@ public static String createQueue(SqsClient sqsClient, String queueName, Boolean Map attrs = new HashMap<>(); attrs.put(QueueAttributeName.FIFO_QUEUE, "true"); CreateQueueRequest createQueueRequest = CreateQueueRequest.builder() - .queueName(queueName) - .attributes(attrs) - .build(); + .queueName(queueName) + .attributes(attrs) + .build(); sqsClient.createQueue(createQueueRequest); System.out.println("\nGet queue url"); GetQueueUrlResponse getQueueUrlResponse = sqsClient - .getQueueUrl(GetQueueUrlRequest.builder().queueName(queueName).build()); + .getQueueUrl(GetQueueUrlRequest.builder().queueName(queueName).build()); return getQueueUrlResponse.queueUrl(); } else { CreateQueueRequest createQueueRequest = CreateQueueRequest.builder() - .queueName(queueName) - .build(); + .queueName(queueName) + .build(); sqsClient.createQueue(createQueueRequest); System.out.println("\nGet queue url"); GetQueueUrlResponse getQueueUrlResponse = sqsClient - .getQueueUrl(GetQueueUrlRequest.builder().queueName(queueName).build()); + .getQueueUrl(GetQueueUrlRequest.builder().queueName(queueName).build()); return getQueueUrlResponse.queueUrl(); } @@ -651,8 +655,8 @@ public static String createSNSTopic(SnsClient snsClient, String topicName) { CreateTopicResponse result; try { CreateTopicRequest request = CreateTopicRequest.builder() - .name(topicName) - .build(); + .name(topicName) + .build(); result = snsClient.createTopic(request); return result.topicArn(); @@ -677,9 +681,9 @@ public static String createFIFO(SnsClient snsClient, String topicName, String du } CreateTopicRequest topicRequest = CreateTopicRequest.builder() - .name(topicName) - .attributes(topicAttributes) - .build(); + .name(topicName) + .attributes(topicAttributes) + .build(); CreateTopicResponse response = snsClient.createTopic(topicRequest); return response.topicArn(); diff --git a/javav2/usecases/topics_and_queues/src/test/java/AWSSNSTest.java b/javav2/usecases/topics_and_queues/src/test/java/AWSSNSTest.java index 89c1e934b76..29b928342a3 100644 --- a/javav2/usecases/topics_and_queues/src/test/java/AWSSNSTest.java +++ b/javav2/usecases/topics_and_queues/src/test/java/AWSSNSTest.java @@ -30,7 +30,7 @@ public void TestWorkflowFIFO() throws InterruptedException { .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) .build(); - String accountId = "814548047983"; + String accountId = ""; String duplication = "n"; String topicName; @@ -161,7 +161,7 @@ public void TestWorkflowNonFIFO() throws InterruptedException { .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) .build(); - String accountId = "814548047983"; + String accountId = ""; String useFIFO; String duplication = "n"; String topicName; diff --git a/kotlin/services/ecr/src/main/kotlin/com/example/ecr/scenario/ECRScenario.kt b/kotlin/services/ecr/src/main/kotlin/com/example/ecr/scenario/ECRScenario.kt index 5901ea85491..2de5e4985df 100644 --- a/kotlin/services/ecr/src/main/kotlin/com/example/ecr/scenario/ECRScenario.kt +++ b/kotlin/services/ecr/src/main/kotlin/com/example/ecr/scenario/ECRScenario.kt @@ -40,14 +40,14 @@ suspend fun main(args: Array) { """.trimIndent() - // if (args.size != 2) { - // println(usage) - // return - // } + if (args.size != 2) { + println(usage) + return + } - var iamRole = "arn:aws:iam::814548047983:role/Admin" + var iamRole = args[0] var localImageName: String - var accountId = "814548047983" + var accountId = args[1] val ecrActions = ECRActions() val scanner = Scanner(System.`in`) diff --git a/kotlin/services/translate/src/main/kotlin/com/kotlin/translate/BatchTranslation.kt b/kotlin/services/translate/src/main/kotlin/com/kotlin/translate/BatchTranslation.kt index 35d63a55f5c..5f974a9cb97 100644 --- a/kotlin/services/translate/src/main/kotlin/com/kotlin/translate/BatchTranslation.kt +++ b/kotlin/services/translate/src/main/kotlin/com/kotlin/translate/BatchTranslation.kt @@ -95,7 +95,7 @@ suspend fun translateDocuments( jobStatus = response.textTranslationJobProperties?.jobStatus.toString() println(jobStatus) - if (jobStatus.contains("COMPLETED")) { + if (jobStatus.contains("Completed")) { break } else { print(".") diff --git a/kotlin/usecases/topics_and_queues/src/test/kotlin/AWSSNSTest.kt b/kotlin/usecases/topics_and_queues/src/test/kotlin/AWSSNSTest.kt index 26d91d5f37b..a5ac01c3b75 100644 --- a/kotlin/usecases/topics_and_queues/src/test/kotlin/AWSSNSTest.kt +++ b/kotlin/usecases/topics_and_queues/src/test/kotlin/AWSSNSTest.kt @@ -141,7 +141,7 @@ class AWSSNSTest { @Test @Order(2) fun testWorkflowNonFIFO() = runBlocking { - val accountId = "814548047983" + val accountId = "" val topicName: String val topicArnVal: String? val sqsQueueName: String From b6e9c0d1139d55ee67444920434f76e9bddc50c4 Mon Sep 17 00:00:00 2001 From: Dennis Traub Date: Wed, 19 Feb 2025 16:15:58 +0100 Subject: [PATCH 057/144] Java: Add Amazon Nova text and image generation examples (#7251) --- .../metadata/bedrock-runtime_metadata.yaml | 54 ++++++++++ javav2/example_code/bedrock-runtime/README.md | 11 +- javav2/example_code/bedrock-runtime/pom.xml | 14 ++- .../bedrockruntime/libs/ImageTools.java | 9 +- .../amazon/nova/canvas/InvokeModel.java | 102 ++++++++++++++++++ .../models/amazon/nova/text/Converse.java | 87 +++++++++++++++ .../amazon/nova/text/ConverseAsync.java | 90 ++++++++++++++++ .../amazon/nova/text/ConverseStream.java | 100 +++++++++++++++++ .../test/java/actions/AbstractModelTest.java | 64 +++++++++++ .../java/actions/IntegrationTestBase.java | 19 ---- .../src/test/java/actions/TestConverse.java | 43 +++----- .../test/java/actions/TestConverseAsync.java | 43 +++----- .../java/actions/TestImageGeneration.java | 22 ++++ .../test/java/actions/TestInvokeModel.java | 83 ++++---------- .../TestInvokeModelWithResponseStream.java | 50 +++------ .../TestAmazonTitanTextScenarios.java | 3 +- 16 files changed, 607 insertions(+), 187 deletions(-) create mode 100644 javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/canvas/InvokeModel.java create mode 100644 javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/Converse.java create mode 100644 javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseAsync.java create mode 100644 javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseStream.java create mode 100644 javav2/example_code/bedrock-runtime/src/test/java/actions/AbstractModelTest.java delete mode 100644 javav2/example_code/bedrock-runtime/src/test/java/actions/IntegrationTestBase.java create mode 100644 javav2/example_code/bedrock-runtime/src/test/java/actions/TestImageGeneration.java diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index 8d11bff779c..d1291811938 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -69,6 +69,26 @@ bedrock-runtime_Converse_Ai21LabsJurassic2: services: bedrock-runtime: {Converse} +bedrock-runtime_Converse_AmazonNovaText: + title: Invoke Amazon Nova on &BR; using Bedrock's Converse API + title_abbrev: "Converse" + synopsis: send a text message to Amazon Nova, using Bedrock's Converse API. + category: Amazon Nova + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova using Bedrock's Converse API with the async Java client. + snippet_tags: + - bedrock-runtime.java2.ConverseAsync_AmazonNovaText + - description: Send a text message to Amazon Nova, using Bedrock's Converse API. + snippet_tags: + - bedrock-runtime.java2.Converse_AmazonNovaText + services: + bedrock-runtime: {Converse} + bedrock-runtime_Converse_AmazonTitanText: title: Invoke Amazon Titan Text on &BR; using Bedrock's Converse API title_abbrev: "Converse" @@ -301,6 +321,23 @@ bedrock-runtime_Converse_Mistral: bedrock-runtime: {Converse} # Converse Stream +bedrock-runtime_ConverseStream_AmazonNovaText: + title: Invoke Amazon Nova on &BR; using Bedrock's Converse API with a response stream + title_abbrev: "ConverseStream" + synopsis: send a text message to Amazon Nova, using Bedrock's Converse API and process the response stream in real-time. + category: Amazon Nova + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova using Bedrock's Converse API and process the response stream in real-time. + snippet_tags: + - bedrock-runtime.java2.ConverseStream_AmazonNovaText + services: + bedrock-runtime: {ConverseStream} + bedrock-runtime_ConverseStream_AmazonTitanText: title: Invoke Amazon Titan Text on &BR; using Bedrock's Converse API with a response stream title_abbrev: "ConverseStream" @@ -1072,6 +1109,23 @@ bedrock-runtime_InvokeModelWithResponseStream_MistralAi: bedrock-runtime: {InvokeModelWithResponseStream} # Image Generation Models +bedrock-runtime_InvokeModel_AmazonNovaImageGeneration: + title: Invoke Amazon Nova Canvas on &BR; to generate an image + title_abbrev: "InvokeModel" + synopsis: invoke Amazon Nova Canvas on &BR; to generate an image. + category: Amazon Nova Canvas + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/bedrock-runtime + excerpts: + - description: Create an image with Amazon Nova Canvas. + snippet_tags: + - bedrock-runtime.java2.InvokeModel_AmazonNovaImageGeneration + services: + bedrock-runtime: {InvokeModel} + bedrock-runtime_InvokeModel_TitanImageGenerator: title: Invoke Amazon Titan Image on &BR; to generate an image title_abbrev: "InvokeModel" diff --git a/javav2/example_code/bedrock-runtime/README.md b/javav2/example_code/bedrock-runtime/README.md index ba38ab71ac1..c1d78a43adb 100644 --- a/javav2/example_code/bedrock-runtime/README.md +++ b/javav2/example_code/bedrock-runtime/README.md @@ -38,6 +38,15 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav - [Converse](src/main/java/com/example/bedrockruntime/models/ai21LabsJurassic2/Converse.java#L6) - [InvokeModel](src/main/java/com/example/bedrockruntime/models/ai21LabsJurassic2/InvokeModel.java#L6) +### Amazon Nova + +- [Converse](src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseAsync.java#L6) +- [ConverseStream](src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseStream.java#L6) + +### Amazon Nova Canvas + +- [InvokeModel](src/main/java/com/example/bedrockruntime/models/amazon/nova/canvas/InvokeModel.java#L6) + ### Amazon Titan Image Generator - [InvokeModel](src/main/java/com/example/bedrockruntime/models/amazonTitanImage/InvokeModel.java#L6) @@ -127,4 +136,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javav2/example_code/bedrock-runtime/pom.xml b/javav2/example_code/bedrock-runtime/pom.xml index 353bd77a0c7..00c9a86fcd3 100644 --- a/javav2/example_code/bedrock-runtime/pom.xml +++ b/javav2/example_code/bedrock-runtime/pom.xml @@ -30,7 +30,7 @@ software.amazon.awssdk bom - 2.28.10 + 2.30.22 pom import @@ -48,12 +48,12 @@ org.json json - 20231013 + 20240303 commons-io commons-io - 2.15.1 + 2.16.1 org.apache.commons @@ -68,7 +68,13 @@ org.junit.jupiter junit-jupiter-api - 5.9.2 + 5.10.2 + test + + + org.junit.jupiter + junit-jupiter-params + 5.10.0 test diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/libs/ImageTools.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/libs/ImageTools.java index a51cd080b10..4f5531042b2 100644 --- a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/libs/ImageTools.java +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/libs/ImageTools.java @@ -15,9 +15,13 @@ public class ImageTools { public static void displayImage(String base64ImageData) { + byte[] imageData = Base64.getDecoder().decode(base64ImageData); + displayImage(imageData); + } + + public static void displayImage(byte[] imageData) { try { - byte[] imageBytes = Base64.getDecoder().decode(base64ImageData); - BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData)); JFrame frame = new JFrame("Image"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -30,5 +34,4 @@ public static void displayImage(String base64ImageData) { throw new RuntimeException(e); } } - } diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/canvas/InvokeModel.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/canvas/InvokeModel.java new file mode 100644 index 00000000000..d09a978f869 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/canvas/InvokeModel.java @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.models.amazon.nova.canvas; + +// snippet-start:[bedrock-runtime.java2.InvokeModel_AmazonNovaImageGeneration] + +import org.json.JSONObject; +import org.json.JSONPointer; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; +import software.amazon.awssdk.services.bedrockruntime.model.InvokeModelResponse; + +import java.security.SecureRandom; +import java.util.Base64; + +import static com.example.bedrockruntime.libs.ImageTools.displayImage; + +/** + * This example demonstrates how to use Amazon Nova Canvas to generate images. + * It shows how to: + * - Set up the Amazon Bedrock runtime client + * - Configure the image generation parameters + * - Send a request to generate an image + * - Process the response and handle the generated image + */ +public class InvokeModel { + + public static byte[] invokeModel() { + + // Step 1: Create the Amazon Bedrock runtime client + // The runtime client handles the communication with AI models on Amazon Bedrock + BedrockRuntimeClient client = BedrockRuntimeClient.builder() + .credentialsProvider(DefaultCredentialsProvider.create()) + .region(Region.US_EAST_1) + .build(); + + // Step 2: Specify which model to use + // For the latest available models, see: + // https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html + String modelId = "amazon.nova-canvas-v1:0"; + + // Step 3: Configure the generation parameters and create the request + // First, set the main parameters: + // - prompt: Text description of the image to generate + // - seed: Random number for reproducible generation (0 to 858,993,459) + String prompt = "A stylized picture of a cute old steampunk robot"; + int seed = new SecureRandom().nextInt(858_993_460); + + // Then, create the request using a template with the following structure: + // - taskType: TEXT_IMAGE (specifies text-to-image generation) + // - textToImageParams: Contains the text prompt + // - imageGenerationConfig: Contains optional generation settings (seed, quality, etc.) + // For a list of available request parameters, see: + // https://docs.aws.amazon.com/nova/latest/userguide/image-gen-req-resp-structure.html + String request = """ + { + "taskType": "TEXT_IMAGE", + "textToImageParams": { + "text": "{{prompt}}" + }, + "imageGenerationConfig": { + "seed": {{seed}}, + "quality": "standard" + } + }""" + .replace("{{prompt}}", prompt) + .replace("{{seed}}", String.valueOf(seed)); + + // Step 4: Send and process the request + // - Send the request to the model using InvokeModelResponse + // - Extract the Base64-encoded image from the JSON response + // - Convert the encoded image to a byte array and return it + try { + InvokeModelResponse response = client.invokeModel(builder -> builder + .modelId(modelId) + .body(SdkBytes.fromUtf8String(request)) + ); + + JSONObject responseBody = new JSONObject(response.body().asUtf8String()); + // Convert the Base64 string to byte array for better handling + return Base64.getDecoder().decode( + new JSONPointer("/images/0").queryFrom(responseBody).toString() + ); + + } catch (SdkClientException e) { + System.err.printf("ERROR: Can't invoke '%s'. Reason: %s%n", modelId, e.getMessage()); + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + System.out.println("Generating image. This may take a few seconds..."); + byte[] imageData = invokeModel(); + displayImage(imageData); + } +} + +// snippet-end:[bedrock-runtime.java2.InvokeModel_AmazonNovaImageGeneration] \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/Converse.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/Converse.java new file mode 100644 index 00000000000..ff6c11f4975 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/Converse.java @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.models.amazon.nova.text; + +// snippet-start:[bedrock-runtime.java2.Converse_AmazonNovaText] + +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; +import software.amazon.awssdk.services.bedrockruntime.model.*; + +/** + * This example demonstrates how to use the Amazon Nova foundation models + * with a synchronous Amazon Bedrock runtime client to generate text. + * It shows how to: + * - Set up the Amazon Bedrock runtime client + * - Create a message + * - Configure and send a request + * - Process the response + */ +public class Converse { + + public static String converse() { + + // Step 1: Create the Amazon Bedrock runtime client + // The runtime client handles the communication with AI models on Amazon Bedrock + BedrockRuntimeClient client = BedrockRuntimeClient.builder() + .credentialsProvider(DefaultCredentialsProvider.create()) + .region(Region.US_EAST_1) + .build(); + + // Step 2: Specify which model to use + // Available Amazon Nova models and their characteristics: + // - Amazon Nova Micro: Text-only model optimized for lowest latency and cost + // - Amazon Nova Lite: Fast, low-cost multimodal model for image, video, and text + // - Amazon Nova Pro: Advanced multimodal model balancing accuracy, speed, and cost + // + // For the latest available models, see: + // https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html + String modelId = "amazon.nova-lite-v1:0"; + + // Step 3: Create the message + // The message includes the text prompt and specifies that it comes from the user + var inputText = "Describe the purpose of a 'hello world' program in one line."; + var message = Message.builder() + .content(ContentBlock.fromText(inputText)) + .role(ConversationRole.USER) + .build(); + + // Step 4: Configure the request + // Optional parameters to control the model's response: + // - maxTokens: maximum number of tokens to generate + // - temperature: randomness (max: 1.0, default: 0.7) + // OR + // - topP: diversity of word choice (max: 1.0, default: 0.9) + // Note: Use either temperature OR topP, but not both + ConverseRequest request = ConverseRequest.builder() + .modelId(modelId) + .messages(message) + .inferenceConfig(config -> config + .maxTokens(500) // The maximum response length + .temperature(0.5F) // Using temperature for randomness control + //.topP(0.9F) // Alternative: use topP instead of temperature + ).build(); + + // Step 5: Send and process the request + // - Send the request to the model + // - Extract and return the generated text from the response + try { + ConverseResponse response = client.converse(request); + return response.output().message().content().get(0).text(); + + } catch (SdkClientException e) { + System.err.printf("ERROR: Can't invoke '%s'. Reason: %s", modelId, e.getMessage()); + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + String response = converse(); + System.out.println(response); + } +} + +// snippet-end:[bedrock-runtime.java2.Converse_AmazonNovaText] \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseAsync.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseAsync.java new file mode 100644 index 00000000000..63bed5262fc --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseAsync.java @@ -0,0 +1,90 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.models.amazon.nova.text; + +// snippet-start:[bedrock-runtime.java2.ConverseAsync_AmazonNovaText] + +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient; +import software.amazon.awssdk.services.bedrockruntime.model.*; + +import java.util.concurrent.CompletableFuture; + +/** + * This example demonstrates how to use the Amazon Nova foundation models + * with an asynchronous Amazon Bedrock runtime client to generate text. + * It shows how to: + * - Set up the Amazon Bedrock runtime client + * - Create a message + * - Configure and send a request + * - Process the response + */ +public class ConverseAsync { + + public static String converseAsync() { + + // Step 1: Create the Amazon Bedrock runtime client + // The runtime client handles the communication with AI models on Amazon Bedrock + BedrockRuntimeAsyncClient client = BedrockRuntimeAsyncClient.builder() + .credentialsProvider(DefaultCredentialsProvider.create()) + .region(Region.US_EAST_1) + .build(); + + // Step 2: Specify which model to use + // Available Amazon Nova models and their characteristics: + // - Amazon Nova Micro: Text-only model optimized for lowest latency and cost + // - Amazon Nova Lite: Fast, low-cost multimodal model for image, video, and text + // - Amazon Nova Pro: Advanced multimodal model balancing accuracy, speed, and cost + // + // For the latest available models, see: + // https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html + String modelId = "amazon.nova-lite-v1:0"; + + // Step 3: Create the message + // The message includes the text prompt and specifies that it comes from the user + var inputText = "Describe the purpose of a 'hello world' program in one line."; + var message = Message.builder() + .content(ContentBlock.fromText(inputText)) + .role(ConversationRole.USER) + .build(); + + // Step 4: Configure the request + // Optional parameters to control the model's response: + // - maxTokens: maximum number of tokens to generate + // - temperature: randomness (max: 1.0, default: 0.7) + // OR + // - topP: diversity of word choice (max: 1.0, default: 0.9) + // Note: Use either temperature OR topP, but not both + ConverseRequest request = ConverseRequest.builder() + .modelId(modelId) + .messages(message) + .inferenceConfig(config -> config + .maxTokens(500) // The maximum response length + .temperature(0.5F) // Using temperature for randomness control + //.topP(0.9F) // Alternative: use topP instead of temperature + ).build(); + + // Step 5: Send and process the request asynchronously + // - Send the request to the model + // - Extract and return the generated text from the response + try { + CompletableFuture asyncResponse = client.converse(request); + return asyncResponse.thenApply( + response -> response.output().message().content().get(0).text() + ).get(); + + } catch (Exception e) { + System.err.printf("Can't invoke '%s': %s", modelId, e.getMessage()); + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + String response = converseAsync(); + System.out.println(response); + } +} + +// snippet-end:[bedrock-runtime.java2.ConverseAsync_AmazonNovaText] \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseStream.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseStream.java new file mode 100644 index 00000000000..e8d129c5539 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/models/amazon/nova/text/ConverseStream.java @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.models.amazon.nova.text; + +// snippet-start:[bedrock-runtime.java2.ConverseStream_AmazonNovaText] + +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient; +import software.amazon.awssdk.services.bedrockruntime.model.*; + +import java.util.concurrent.ExecutionException; + +/** + * This example demonstrates how to use the Amazon Nova foundation models with an + * asynchronous Amazon Bedrock runtime client to generate streaming text responses. + * It shows how to: + * - Set up the Amazon Bedrock runtime client + * - Create a message + * - Configure a streaming request + * - Set up a stream handler to process the response chunks + * - Process the streaming response + */ +public class ConverseStream { + + public static void converseStream() { + + // Step 1: Create the Amazon Bedrock runtime client + // The runtime client handles the communication with AI models on Amazon Bedrock + BedrockRuntimeAsyncClient client = BedrockRuntimeAsyncClient.builder() + .credentialsProvider(DefaultCredentialsProvider.create()) + .region(Region.US_EAST_1) + .build(); + + // Step 2: Specify which model to use + // Available Amazon Nova models and their characteristics: + // - Amazon Nova Micro: Text-only model optimized for lowest latency and cost + // - Amazon Nova Lite: Fast, low-cost multimodal model for image, video, and text + // - Amazon Nova Pro: Advanced multimodal model balancing accuracy, speed, and cost + // + // For the latest available models, see: + // https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html + String modelId = "amazon.nova-lite-v1:0"; + + // Step 3: Create the message + // The message includes the text prompt and specifies that it comes from the user + var inputText = "Describe the purpose of a 'hello world' program in one paragraph"; + var message = Message.builder() + .content(ContentBlock.fromText(inputText)) + .role(ConversationRole.USER) + .build(); + + // Step 4: Configure the request + // Optional parameters to control the model's response: + // - maxTokens: maximum number of tokens to generate + // - temperature: randomness (max: 1.0, default: 0.7) + // OR + // - topP: diversity of word choice (max: 1.0, default: 0.9) + // Note: Use either temperature OR topP, but not both + ConverseStreamRequest request = ConverseStreamRequest.builder() + .modelId(modelId) + .messages(message) + .inferenceConfig(config -> config + .maxTokens(500) // The maximum response length + .temperature(0.5F) // Using temperature for randomness control + //.topP(0.9F) // Alternative: use topP instead of temperature + ).build(); + + // Step 5: Set up the stream handler + // The stream handler processes chunks of the response as they arrive + // - onContentBlockDelta: Processes each text chunk + // - onError: Handles any errors during streaming + var streamHandler = ConverseStreamResponseHandler.builder() + .subscriber(ConverseStreamResponseHandler.Visitor.builder() + .onContentBlockDelta(chunk -> { + System.out.print(chunk.delta().text()); + System.out.flush(); // Ensure immediate output of each chunk + }).build()) + .onError(err -> System.err.printf("Can't invoke '%s': %s", modelId, err.getMessage())) + .build(); + + // Step 6: Send the streaming request and process the response + // - Send the request to the model + // - Attach the handler to process response chunks as they arrive + // - Handle any errors during streaming + try { + client.converseStream(request, streamHandler).get(); + + } catch (ExecutionException | InterruptedException e) { + System.err.printf("Can't invoke '%s': %s", modelId, e.getCause().getMessage()); + } + } + + public static void main(String[] args) { + converseStream(); + } +} + +// snippet-end:[bedrock-runtime.java2.ConverseStream_AmazonNovaText] \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/AbstractModelTest.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/AbstractModelTest.java new file mode 100644 index 00000000000..02861891fe6 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/test/java/actions/AbstractModelTest.java @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package actions; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractModelTest { + + /** + * Provide the model classes to test. + * Each concrete test class must implement this method. + */ + protected abstract Stream modelProvider(); + + /** + * Provide the method name to test. + * Each concrete test class must implement this method. + */ + protected abstract String getMethodName(); + + /** + * Validates the result of the model invocation. + * Can be overridden by concrete classes if needed. + */ + protected void validateResult(Object result, String modelName) { + if (result instanceof String) { + assertFalse(Objects.requireNonNull((String) result).trim().isEmpty(), + "Empty result from " + modelName); + } else if (result instanceof byte[]) { + assertNotEquals(0, Objects.requireNonNull((byte[]) result).length, + "Empty result from " + modelName); + } else { + fail("Unexpected result type from " + modelName + ": " + result.getClass()); + } + } + + @ParameterizedTest(name = "Test {0}") + @MethodSource("modelProvider") + void testModel(ModelTest model) { + try { + Object result = model.cls().getMethod(getMethodName()).invoke(null); + validateResult(result, model.name()); + + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + fail("Test failed for " + model.name() + ": " + cause.getMessage(), cause); + } catch (NoSuchMethodException | IllegalAccessException e) { + fail("Test configuration error for " + model.name() + ": " + e.getMessage(), e); + } + } + + protected record ModelTest(String name, Class cls) { + } +} \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/IntegrationTestBase.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/IntegrationTestBase.java deleted file mode 100644 index ff3c70bbbeb..00000000000 --- a/javav2/example_code/bedrock-runtime/src/test/java/actions/IntegrationTestBase.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package actions; - -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.TestInstance; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@Tag("IntegrationTest") -@TestInstance(TestInstance.Lifecycle.PER_METHOD) -public abstract class IntegrationTestBase { - protected void assertNotNullOrEmpty(String string) { - assertNotNull(string); - assertFalse(string.trim().isEmpty()); - } -} diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverse.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverse.java index 889de5b62d7..861379eb83b 100644 --- a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverse.java +++ b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverse.java @@ -3,36 +3,21 @@ package actions; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; -public class TestConverse extends IntegrationTestBase { - @Test - void testJurassic2() { - String result = com.example.bedrockruntime.models.ai21LabsJurassic2.Converse.converse(); - assertNotNullOrEmpty(result); +public class TestConverse extends AbstractModelTest { + protected String getMethodName() { + return "converse"; } - @Test - void testTitanText() { - String result = com.example.bedrockruntime.models.amazonTitanText.Converse.converse(); - assertNotNullOrEmpty(result); + protected Stream modelProvider() { + return Stream.of( + new ModelTest("Claude", com.example.bedrockruntime.models.anthropicClaude.Converse.class), + new ModelTest("CohereCommand", com.example.bedrockruntime.models.cohereCommand.Converse.class), + new ModelTest("Jurassic2", com.example.bedrockruntime.models.ai21LabsJurassic2.Converse.class), + new ModelTest("Mistral", com.example.bedrockruntime.models.mistral.Converse.class), + new ModelTest("NovaText", com.example.bedrockruntime.models.amazon.nova.text.Converse.class), + new ModelTest("TitanText", com.example.bedrockruntime.models.amazonTitanText.Converse.class) + ); } - - @Test - void testClaude() { - String result = com.example.bedrockruntime.models.anthropicClaude.Converse.converse(); - assertNotNullOrEmpty(result); - } - - @Test - void testCohereCommand() { - String result = com.example.bedrockruntime.models.cohereCommand.Converse.converse(); - assertNotNullOrEmpty(result); - } - - @Test - void testMistral() { - String result = com.example.bedrockruntime.models.mistral.Converse.converse(); - assertNotNullOrEmpty(result); - } -} +} \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverseAsync.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverseAsync.java index 6b36a7e1b5a..ea814d33c7d 100644 --- a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverseAsync.java +++ b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestConverseAsync.java @@ -3,36 +3,21 @@ package actions; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; -public class TestConverseAsync extends IntegrationTestBase { - @Test - void testJurassic2() { - String result = com.example.bedrockruntime.models.ai21LabsJurassic2.ConverseAsync.converseAsync(); - assertNotNullOrEmpty(result); +public class TestConverseAsync extends AbstractModelTest { + protected String getMethodName() { + return "converseAsync"; } - @Test - void testTitanText() { - String result = com.example.bedrockruntime.models.amazonTitanText.ConverseAsync.converseAsync(); - assertNotNullOrEmpty(result); + protected Stream modelProvider() { + return Stream.of( + new TestConverseAsync.ModelTest("Jurassic2", com.example.bedrockruntime.models.ai21LabsJurassic2.ConverseAsync.class), + new TestConverseAsync.ModelTest("NovaText", com.example.bedrockruntime.models.amazon.nova.text.ConverseAsync.class), + new TestConverseAsync.ModelTest("TitanText", com.example.bedrockruntime.models.amazonTitanText.ConverseAsync.class), + new TestConverseAsync.ModelTest("Claude", com.example.bedrockruntime.models.anthropicClaude.ConverseAsync.class), + new TestConverseAsync.ModelTest("CohereCommand", com.example.bedrockruntime.models.cohereCommand.ConverseAsync.class), + new TestConverseAsync.ModelTest("Mistral", com.example.bedrockruntime.models.mistral.ConverseAsync.class) + ); } - - @Test - void testClaude() { - String result = com.example.bedrockruntime.models.anthropicClaude.ConverseAsync.converseAsync(); - assertNotNullOrEmpty(result); - } - - @Test - void testCohereCommand() { - String result = com.example.bedrockruntime.models.cohereCommand.ConverseAsync.converseAsync(); - assertNotNullOrEmpty(result); - } - - @Test - void testMistral() { - String result = com.example.bedrockruntime.models.mistral.ConverseAsync.converseAsync(); - assertNotNullOrEmpty(result); - } -} +} \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestImageGeneration.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestImageGeneration.java new file mode 100644 index 00000000000..3ed2cf58f77 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestImageGeneration.java @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package actions; + +import java.util.stream.Stream; + +public class TestImageGeneration extends AbstractModelTest { + @Override + protected String getMethodName() { + return "invokeModel"; + } + + @Override + protected Stream modelProvider() { + return Stream.of( + new TestInvokeModel.ModelTest("NovaCanvas", com.example.bedrockruntime.models.amazon.nova.canvas.InvokeModel.class), + new TestInvokeModel.ModelTest("StableDiffusion", com.example.bedrockruntime.models.stabilityAi.InvokeModel.class), + new TestInvokeModel.ModelTest("TitanImage", com.example.bedrockruntime.models.amazonTitanText.InvokeModel.class) + ); + } +} \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModel.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModel.java index e446933d547..b80d83ea6ea 100644 --- a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModel.java +++ b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModel.java @@ -3,66 +3,23 @@ package actions; -import org.junit.jupiter.api.Test; - -public class TestInvokeModel extends IntegrationTestBase { - @Test - void testJurassic2() { - String result = com.example.bedrockruntime.models.ai21LabsJurassic2.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testTitanImage() { - String result = com.example.bedrockruntime.models.amazonTitanImage.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testTitanText() { - String result = com.example.bedrockruntime.models.amazonTitanText.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testTitanTextEmbeddings() { - String result = com.example.bedrockruntime.models.amazonTitanTextEmbeddings.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testClaude() { - String result = com.example.bedrockruntime.models.anthropicClaude.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testCohereCommand() { - String result = com.example.bedrockruntime.models.cohereCommand.Command_InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testCohereCommandR() { - String result = com.example.bedrockruntime.models.cohereCommand.Command_R_InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testLlama3() { - String result = com.example.bedrockruntime.models.metaLlama.Llama3_InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testMistral() { - String result = com.example.bedrockruntime.models.mistral.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } - - @Test - void testStableDiffusion() { - String result = com.example.bedrockruntime.models.stabilityAi.InvokeModel.invokeModel(); - assertNotNullOrEmpty(result); - } -} +import java.util.stream.Stream; + +public class TestInvokeModel extends AbstractModelTest { + protected String getMethodName() { + return "invokeModel"; + } + + protected Stream modelProvider() { + return Stream.of( + new TestInvokeModel.ModelTest("Claude", com.example.bedrockruntime.models.anthropicClaude.InvokeModel.class), + new TestInvokeModel.ModelTest("CohereCommand", com.example.bedrockruntime.models.cohereCommand.Command_InvokeModel.class), + new TestInvokeModel.ModelTest("CohereCommandR", com.example.bedrockruntime.models.cohereCommand.Command_R_InvokeModel.class), + new TestInvokeModel.ModelTest("Jurassic2", com.example.bedrockruntime.models.ai21LabsJurassic2.InvokeModel.class), + new TestInvokeModel.ModelTest("Llama", com.example.bedrockruntime.models.metaLlama.Llama3_InvokeModel.class), + new TestInvokeModel.ModelTest("Mistral", com.example.bedrockruntime.models.mistral.InvokeModel.class), + new TestInvokeModel.ModelTest("TitanText", com.example.bedrockruntime.models.amazonTitanText.InvokeModel.class), + new TestInvokeModel.ModelTest("TitanTextEmbeddings", com.example.bedrockruntime.models.amazonTitanText.InvokeModel.class) + ); + } +} \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModelWithResponseStream.java b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModelWithResponseStream.java index 76cb1983dea..4fc4669e5e8 100644 --- a/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModelWithResponseStream.java +++ b/javav2/example_code/bedrock-runtime/src/test/java/actions/TestInvokeModelWithResponseStream.java @@ -3,45 +3,21 @@ package actions; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; -import java.util.concurrent.ExecutionException; - -public class TestInvokeModelWithResponseStream extends IntegrationTestBase { - - @Test - void testTitanText() throws ExecutionException, InterruptedException { - String result = com.example.bedrockruntime.models.amazonTitanText.InvokeModelWithResponseStream.invokeModelWithResponseStream(); - assertNotNullOrEmpty(result); - } - - @Test - void testClaude() throws ExecutionException, InterruptedException { - String result = com.example.bedrockruntime.models.anthropicClaude.InvokeModelWithResponseStream.invokeModelWithResponseStream(); - assertNotNullOrEmpty(result); - } - - @Test - void testCohereCommand() throws ExecutionException, InterruptedException { - String result = com.example.bedrockruntime.models.cohereCommand.Command_InvokeModelWithResponseStream.invokeModelWithResponseStream(); - assertNotNullOrEmpty(result); - } - - @Test - void testCohereCommandR() throws ExecutionException, InterruptedException { - String result = com.example.bedrockruntime.models.cohereCommand.Command_R_InvokeModelWithResponseStream.invokeModelWithResponseStream(); - assertNotNullOrEmpty(result); - } - - @Test - void testLlama3() { - String result = com.example.bedrockruntime.models.metaLlama.Llama3_InvokeModelWithResponseStream.invokeModelWithResponseStream(); - assertNotNullOrEmpty(result); +public class TestInvokeModelWithResponseStream extends AbstractModelTest { + protected String getMethodName() { + return "invokeModelWithResponseStream"; } - @Test - void testMistral() throws ExecutionException, InterruptedException { - String result = com.example.bedrockruntime.models.mistral.InvokeModelWithResponseStream.invokeModelWithResponseStream(); - assertNotNullOrEmpty(result); + protected Stream modelProvider() { + return Stream.of( + new TestInvokeModel.ModelTest("Claude", com.example.bedrockruntime.models.anthropicClaude.InvokeModelWithResponseStream.class), + new TestInvokeModel.ModelTest("CohereCommand", com.example.bedrockruntime.models.cohereCommand.Command_InvokeModelWithResponseStream.class), + new TestInvokeModel.ModelTest("CohereCommandR", com.example.bedrockruntime.models.cohereCommand.Command_R_InvokeModelWithResponseStream.class), + new TestInvokeModel.ModelTest("Llama", com.example.bedrockruntime.models.metaLlama.Llama3_InvokeModelWithResponseStream.class), + new TestInvokeModel.ModelTest("Mistral", com.example.bedrockruntime.models.mistral.InvokeModelWithResponseStream.class), + new TestInvokeModel.ModelTest("TitanText", com.example.bedrockruntime.models.amazonTitanText.InvokeModelWithResponseStream.class) + ); } } diff --git a/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestAmazonTitanTextScenarios.java b/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestAmazonTitanTextScenarios.java index 3eda004aac4..2787bf67c9a 100644 --- a/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestAmazonTitanTextScenarios.java +++ b/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestAmazonTitanTextScenarios.java @@ -3,14 +3,13 @@ package scenarios; -import actions.IntegrationTestBase; import org.junit.jupiter.api.Test; import static com.example.bedrockruntime.models.amazonTitanText.TextScenarios.invokeWithConversation; import static com.example.bedrockruntime.models.amazonTitanText.TextScenarios.invokeWithSystemPrompt; import static org.junit.jupiter.api.Assertions.assertFalse; -class TestAmazonTitanTextScenarios extends IntegrationTestBase { +class TestAmazonTitanTextScenarios { @Test void invokeWithSystemPromptScenario() { From c886f9d615299ccf56b2ad611163e1003aea5236 Mon Sep 17 00:00:00 2001 From: Chris Rees <34663864+AWSChris@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:10:10 -0800 Subject: [PATCH 058/144] Python: Added Hello bedrock runtime examples for Amazon Bedrock (#7244) --------- Co-authored-by: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> --- .../metadata/bedrock-runtime_metadata.yaml | 14 +++ python/example_code/bedrock-runtime/README.md | 14 ++- .../hello/hello_bedrock_runtime_converse.py | 83 ++++++++++++++++++ .../hello/hello_bedrock_runtime_invoke.py | 87 +++++++++++++++++++ .../test/test_hello_bedrock_runtime.py | 23 +++++ .../test/test_hello_bedrock_runtime_invoke.py | 24 +++++ 6 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_converse.py create mode 100644 python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_invoke.py create mode 100644 python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime.py create mode 100644 python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime_invoke.py diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index d1291811938..2e8de4e4b5e 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -21,6 +21,20 @@ bedrock-runtime_Hello: - description: snippet_files: - javascriptv3/example_code/bedrock-runtime/hello.js + + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-runtime + sdkguide: + excerpts: + - description: Send a prompt to a model with the InvokeModel operation. + snippet_tags: + - bedrock-runtime.example_code.hello_bedrock_invoke.complete + - description: Send a user message to a model with the Converse operation. + snippet_tags: + - bedrock-runtime.example_code.hello_bedrock_converse.complete + services: bedrock-runtime: {InvokeModel} diff --git a/python/example_code/bedrock-runtime/README.md b/python/example_code/bedrock-runtime/README.md index 03eb70ca24b..dce1178412d 100644 --- a/python/example_code/bedrock-runtime/README.md +++ b/python/example_code/bedrock-runtime/README.md @@ -38,6 +38,11 @@ python -m pip install -r requirements.txt > see [Model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). > + +### Get started + +- [Hello Amazon Bedrock Runtime](hello/hello_bedrock_runtime_invoke.py#L5) (`InvokeModel`) + ### AI21 Labs Jurassic-2 - [Converse](models/ai21_labs_jurassic2/converse.py#L4) @@ -120,6 +125,13 @@ Mistral AI. +#### Hello Amazon Bedrock Runtime + +This example shows you how to get started using Amazon Bedrock Runtime. + +``` +python hello/hello_bedrock_runtime_invoke.py +``` ### Tests @@ -148,4 +160,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_converse.py b/python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_converse.py new file mode 100644 index 00000000000..e6139428532 --- /dev/null +++ b/python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_converse.py @@ -0,0 +1,83 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + + +# snippet-start:[bedrock-runtime.example_code.hello_bedrock_converse.complete] + +""" +Uses the Amazon Bedrock runtime client Converse operation to send a user message to a model. +""" +import logging +import boto3 + +from botocore.exceptions import ClientError + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def converse(brt, model_id, user_message): + """ + Uses the Converse operation to send a user message to the supplied model. + param brt: A bedrock runtime boto3 client + param model_id: The model ID for the model that you want to use. + param user message: The user message that you want to send to the model. + + :return: The text response from the model. + """ + + # Format the request payload using the model's native structure. + conversation = [ + { + "role": "user", + "content": [{"text": user_message}], + } +] + + try: + # Send the message to the model, using a basic inference configuration. + response = brt.converse( + modelId=model_id, + messages=conversation, + inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9}, + ) + + # Extract and print the response text. + response_text = response["output"]["message"]["content"][0]["text"] + return response_text + + except (ClientError, Exception) as e: + print(f"ERROR: Can't invoke '{model_id}'. Reason: {e}") + raise + + +def main(): + """Entry point for the example. Uses the AWS SDK for Python (Boto3) + to create an Amazon Bedrock runtime client. Then sends a user message to a model + in the region set in the callers profile and credentials. + """ + + # Create an Amazon Bedrock Runtime client. + brt = boto3.client("bedrock-runtime") + + # Set the model ID, e.g., Amazon Titan Text G1 - Express. + model_id = "amazon.titan-text-express-v1" + + # Define the message for the model. + message = "Describe the purpose of a 'hello world' program in one line." + + # Send the message to the model. + response = converse(brt, model_id, message) + + print(f"Response: {response}") + + logger.info("Done.") + + +if __name__ == "__main__": + main() + + # snippet-end:[bedrock-runtime.example_code.hello_bedrock_converse.complete] + + diff --git a/python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_invoke.py b/python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_invoke.py new file mode 100644 index 00000000000..d86268579dc --- /dev/null +++ b/python/example_code/bedrock-runtime/hello/hello_bedrock_runtime_invoke.py @@ -0,0 +1,87 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + + +# snippet-start:[bedrock-runtime.example_code.hello_bedrock_invoke.complete] + +""" +Uses the Amazon Bedrock runtime client InvokeModel operation to send a prompt to a model. +""" +import logging +import json +import boto3 + + +from botocore.exceptions import ClientError + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def invoke_model(brt, model_id, prompt): + """ + Invokes the specified model with the supplied prompt. + param brt: A bedrock runtime boto3 client + param model_id: The model ID for the model that you want to use. + param prompt: The prompt that you want to send to the model. + + :return: The text response from the model. + """ + + # Format the request payload using the model's native structure. + native_request = { + "inputText": prompt, + "textGenerationConfig": { + "maxTokenCount": 512, + "temperature": 0.5, + "topP": 0.9 + } + } + + # Convert the native request to JSON. + request = json.dumps(native_request) + + try: + # Invoke the model with the request. + response = brt.invoke_model(modelId=model_id, body=request) + + # Decode the response body. + model_response = json.loads(response["body"].read()) + + # Extract and print the response text. + response_text = model_response["results"][0]["outputText"] + return response_text + + except (ClientError, Exception) as e: + print(f"ERROR: Can't invoke '{model_id}'. Reason: {e}") + raise + + +def main(): + """Entry point for the example. Uses the AWS SDK for Python (Boto3) + to create an Amazon Bedrock runtime client. Then sends a prompt to a model + in the region set in the callers profile and credentials. + """ + + # Create an Amazon Bedrock Runtime client. + brt = boto3.client("bedrock-runtime") + + # Set the model ID, e.g., Amazon Titan Text G1 - Express. + model_id = "amazon.titan-text-express-v1" + + # Define the prompt for the model. + prompt = "Describe the purpose of a 'hello world' program in one line." + + # Send the prompt to the model. + response = invoke_model(brt, model_id, prompt) + + print(f"Response: {response}") + + logger.info("Done.") + + +if __name__ == "__main__": + main() + + # snippet-end:[bedrock-runtime.example_code.hello_bedrock_invoke.complete] diff --git a/python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime.py b/python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime.py new file mode 100644 index 00000000000..f69602ba462 --- /dev/null +++ b/python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime.py @@ -0,0 +1,23 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import subprocess +import sys + +files_under_test = [ + # Text models + "hello/hello_bedrock_runtime_invoke.py" +] + + +@pytest.mark.integ +@pytest.mark.parametrize("file", files_under_test) +def test_hello_bedrock(file): + result = subprocess.run( + [sys.executable, file], + capture_output=True, + text=True, + ) + assert result.stdout != "" + assert result.returncode == 0 diff --git a/python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime_invoke.py b/python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime_invoke.py new file mode 100644 index 00000000000..8de7d56b722 --- /dev/null +++ b/python/example_code/bedrock-runtime/test/test_hello_bedrock_runtime_invoke.py @@ -0,0 +1,24 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import subprocess +import sys + +files_under_test = [ + # Text models + "hello/hello_bedrock_runtime_invoke.py", + "hello/hello_bedrock_runtime_converse.py" +] + + +@pytest.mark.integ +@pytest.mark.parametrize("file", files_under_test) +def test_hello_bedrock(file): + result = subprocess.run( + [sys.executable, file], + capture_output=True, + text=True, + ) + assert result.stdout != "" + assert result.returncode == 0 From c32ed3cde0c9ceeb932058dca2d1b4f3a8e0f782 Mon Sep 17 00:00:00 2001 From: Scott Macdonald <57190223+scmacdon@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:50:36 -0500 Subject: [PATCH 059/144] Basic scenario metadata update (#7200) * updated the Sitewise YAML * updated the Sitewise YAML * updated the IoT YAML * updated the IoT YAML * updated the Redshift YAML * updated the SSM YAML * updated the SSM YAML * updated the SSM YAML * updated the SSM YAML * updated the SSM YAML * updated the SSM YAML * updated the Batch YAML * updated SSM Readme * updated Batch \Readme * updated Iot YAML * updated Readme * updated Readmes * updated Sitewise Readme --- .doc_gen/metadata/batch_metadata.yaml | 4 +-- .doc_gen/metadata/iot_metadata.yaml | 13 ++++++++- .doc_gen/metadata/iot_sitewise_metadata.yaml | 11 +++++++- .doc_gen/metadata/redshift_metadata.yaml | 9 +++++- .doc_gen/metadata/ssm_metadata.yaml | 9 +++++- cpp/example_code/iot/README.md | 17 +++++++++-- gov2/redshift/README.md | 11 ++++++-- .../example_code/iotsitewise/README.md | 13 +++++++-- javascriptv3/example_code/ssm/README.md | 11 ++++++-- javav2/example_code/batch/README.md | 28 +++++++++---------- javav2/example_code/iot/README.md | 17 +++++++++-- javav2/example_code/iotsitewise/README.md | 13 +++++++-- javav2/example_code/redshift/README.md | 11 ++++++-- javav2/example_code/ssm/README.md | 11 ++++++-- kotlin/services/iot/README.md | 17 +++++++++-- python/example_code/iotsitewise/README.md | 13 +++++++-- python/example_code/redshift/README.md | 11 ++++++-- python/example_code/ssm/README.md | 11 ++++++-- 18 files changed, 182 insertions(+), 48 deletions(-) diff --git a/.doc_gen/metadata/batch_metadata.yaml b/.doc_gen/metadata/batch_metadata.yaml index e50b4b1594b..3d3c07b1ddc 100644 --- a/.doc_gen/metadata/batch_metadata.yaml +++ b/.doc_gen/metadata/batch_metadata.yaml @@ -174,8 +174,6 @@ batch_CreateComputeEnvironment: services: batch: {CreateComputeEnvironment} batch_Scenario: - title: Learn core operations for'&BATCHlong; using an &AWS; SDK - title_abbrev: Learn &BATCH; core operations synopsis_list: - Create an &BATCH; compute environment. - Check the status of the compute environment. @@ -185,7 +183,7 @@ batch_Scenario: - Get a list of jobs applicable to the job queue. - Check the status of job. - Delete &BATCH; resources. - category: Scenarios + category: Basics languages: Java: versions: diff --git a/.doc_gen/metadata/iot_metadata.yaml b/.doc_gen/metadata/iot_metadata.yaml index 8c75fada4d3..6339c137a6f 100644 --- a/.doc_gen/metadata/iot_metadata.yaml +++ b/.doc_gen/metadata/iot_metadata.yaml @@ -429,7 +429,18 @@ iot_CreateThing: services: iot: {CreateThing} iot_Scenario: - synopsis: work with &IoT; device management. + synopsis_list: + - Create an &IoT; Thing. + - Generate a device certificate. + - Update an &IoT; Thing with Attributes. + - Return a unique endpoint. + - List your &IoT; certificates. + - Create an &IoT; shadow. + - Write out state information. + - Creates a rule. + - List your rules. + - Search things using the Thing name. + - Delete an &IoT; Thing. category: Basics languages: Kotlin: diff --git a/.doc_gen/metadata/iot_sitewise_metadata.yaml b/.doc_gen/metadata/iot_sitewise_metadata.yaml index 6b067573271..d6dd34950e5 100644 --- a/.doc_gen/metadata/iot_sitewise_metadata.yaml +++ b/.doc_gen/metadata/iot_sitewise_metadata.yaml @@ -444,7 +444,16 @@ iotsitewise_CreateAssetModel: services: iotsitewise: {CreateAssetModel} iotsitewise_Scenario: - synopsis: learn core operations for &ITSWlong; using an &AWS; SDK. + synopsis_list: + - Create an &ITSWlong; Asset Model. + - Create an &ITSWlong; Asset. + - Retrieve the property ID values. + - Send data to an &ITSWlong; Asset. + - Retrieve the value of the &ITSWlong; Asset property. + - Create an &ITSWlong; Portal. + - Create an &ITSWlong; Gateway. + - Describe the &ITSWlong; Gateway. + - Delete the &ITSWlong; Assets. category: Basics languages: Java: diff --git a/.doc_gen/metadata/redshift_metadata.yaml b/.doc_gen/metadata/redshift_metadata.yaml index 2f4392bf8b7..d9627603d72 100644 --- a/.doc_gen/metadata/redshift_metadata.yaml +++ b/.doc_gen/metadata/redshift_metadata.yaml @@ -346,7 +346,14 @@ redshift_ExecuteStatement: services: redshift: {ExecuteStatement} redshift_Scenario: - synopsis: learn core operations for &RS; using an &AWS; SDK. + synopsis_list: + - Create a Redshift cluster. + - List databases in the cluster. + - Create a table named Movies. + - Populate the Movies table. + - Query the Movies table by year. + - Modify the Redshift cluster. + - Delete the Amazon Redshift cluster. category: Basics languages: Go: diff --git a/.doc_gen/metadata/ssm_metadata.yaml b/.doc_gen/metadata/ssm_metadata.yaml index b3cc04d35b1..ed81ab102a8 100644 --- a/.doc_gen/metadata/ssm_metadata.yaml +++ b/.doc_gen/metadata/ssm_metadata.yaml @@ -412,7 +412,14 @@ ssm_UpdateOpsItem: services: ssm: {UpdateOpsItem} ssm_Scenario: - synopsis: work with &SYS; maintenance windows, documents, and OpsItems. + synopsis_list: + - Create a maintenance window. + - Modify the maintenance window schedule. + - Create a document. + - Send a command to a specified EC2 instance. + - Create an OpsItem. + - Update and resolve the OpsItem. + - Delete the maintenance window, OpsItem, and document. category: Basics languages: Java: diff --git a/cpp/example_code/iot/README.md b/cpp/example_code/iot/README.md index 9839fed6638..9497e020860 100644 --- a/cpp/example_code/iot/README.md +++ b/cpp/example_code/iot/README.md @@ -99,8 +99,19 @@ This example shows you how to get started using AWS IoT. #### Learn the basics -This example shows you how to work with AWS IoT device management. - +This example shows you how to do the following: + +- Create an AWS IoT Thing. +- Generate a device certificate. +- Update an AWS IoT Thing with Attributes. +- Return a unique endpoint. +- List your AWS IoT certificates. +- Create an AWS IoT shadow. +- Write out state information. +- Creates a rule. +- List your rules. +- Search things using the Thing name. +- Delete an AWS IoT Thing. @@ -140,4 +151,4 @@ This example shows you how to work with AWS IoT device management. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/gov2/redshift/README.md b/gov2/redshift/README.md index 28b7417b7ee..95df3003dfa 100644 --- a/gov2/redshift/README.md +++ b/gov2/redshift/README.md @@ -80,8 +80,15 @@ go run ./cmd -h ``` #### Learn the basics -This example shows you how to learn core operations for Amazon Redshift using an AWS SDK. +This example shows you how to do the following: +- Create a Redshift cluster. +- List databases in the cluster. +- Create a table named Movies. +- Populate the Movies table. +- Query the Movies table by year. +- Modify the Redshift cluster. +- Delete the Amazon Redshift cluster. @@ -117,4 +124,4 @@ in the `gov2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javascriptv3/example_code/iotsitewise/README.md b/javascriptv3/example_code/iotsitewise/README.md index c4e88a366b7..f48868b20ca 100644 --- a/javascriptv3/example_code/iotsitewise/README.md +++ b/javascriptv3/example_code/iotsitewise/README.md @@ -107,8 +107,17 @@ node ./hello.js #### Learn the basics -This example shows you how to learn core operations for AWS IoT SiteWise using an AWS SDK. - +This example shows you how to do the following: + +- Create an AWS IoT SiteWise Asset Model. +- Create an AWS IoT SiteWise Asset. +- Retrieve the property ID values. +- Send data to an AWS IoT SiteWise Asset. +- Retrieve the value of the AWS IoT SiteWise Asset property. +- Create an AWS IoT SiteWise Portal. +- Create an AWS IoT SiteWise Gateway. +- Describe the AWS IoT SiteWise Gateway. +- Delete the AWS IoT SiteWise Assets. diff --git a/javascriptv3/example_code/ssm/README.md b/javascriptv3/example_code/ssm/README.md index 29d49814001..e5aad1ec993 100644 --- a/javascriptv3/example_code/ssm/README.md +++ b/javascriptv3/example_code/ssm/README.md @@ -103,8 +103,15 @@ node ./hello.js #### Learn the basics -This example shows you how to work with Systems Manager maintenance windows, documents, and OpsItems. +This example shows you how to do the following: +- Create a maintenance window. +- Modify the maintenance window schedule. +- Create a document. +- Send a command to a specified EC2 instance. +- Create an OpsItem. +- Update and resolve the OpsItem. +- Delete the maintenance window, OpsItem, and document. @@ -140,4 +147,4 @@ in the `javascriptv3` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javav2/example_code/batch/README.md b/javav2/example_code/batch/README.md index 91c66ea73bd..5a11d043da6 100644 --- a/javav2/example_code/batch/README.md +++ b/javav2/example_code/batch/README.md @@ -34,6 +34,13 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav - [Hello AWS Batch](src/main/java/com/example/batch/HelloBatch.java#L6) (`listJobsPaginator`) +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](src/main/java/com/example/batch/scenario/BatchScenario.java) + + ### Single actions Code excerpts that show you how to call individual service functions. @@ -52,13 +59,6 @@ Code excerpts that show you how to call individual service functions. - [UpdateComputeEnvironment](src/main/java/com/example/batch/scenario/BatchActions.java#L439) - [UpdateJobQueue](src/main/java/com/example/batch/scenario/BatchActions.java#L347) -### Scenarios - -Code examples that show you how to accomplish a specific task by calling multiple -functions within the same service. - -- [Learn AWS Batch core operations](src/main/java/com/example/batch/scenario/BatchScenario.java) - @@ -76,8 +76,7 @@ functions within the same service. This example shows you how to get started using AWS Batch. - -#### Learn AWS Batch core operations +#### Learn the basics This example shows you how to do the following: @@ -90,12 +89,13 @@ This example shows you how to do the following: - Check the status of job. - Delete AWS Batch resources. - - + + + + + - - ### Tests @@ -123,4 +123,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javav2/example_code/iot/README.md b/javav2/example_code/iot/README.md index e77c9b3515a..5e99310cd7d 100644 --- a/javav2/example_code/iot/README.md +++ b/javav2/example_code/iot/README.md @@ -77,8 +77,19 @@ This example shows you how to get started using AWS IoT. #### Learn the basics -This example shows you how to work with AWS IoT device management. - +This example shows you how to do the following: + +- Create an AWS IoT Thing. +- Generate a device certificate. +- Update an AWS IoT Thing with Attributes. +- Return a unique endpoint. +- List your AWS IoT certificates. +- Create an AWS IoT shadow. +- Write out state information. +- Creates a rule. +- List your rules. +- Search things using the Thing name. +- Delete an AWS IoT Thing. @@ -114,4 +125,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javav2/example_code/iotsitewise/README.md b/javav2/example_code/iotsitewise/README.md index 907d087a67a..a513b574029 100644 --- a/javav2/example_code/iotsitewise/README.md +++ b/javav2/example_code/iotsitewise/README.md @@ -79,8 +79,17 @@ This example shows you how to get started using AWS IoT SiteWise. #### Learn the basics -This example shows you how to learn core operations for AWS IoT SiteWise using an AWS SDK. - +This example shows you how to do the following: + +- Create an AWS IoT SiteWise Asset Model. +- Create an AWS IoT SiteWise Asset. +- Retrieve the property ID values. +- Send data to an AWS IoT SiteWise Asset. +- Retrieve the value of the AWS IoT SiteWise Asset property. +- Create an AWS IoT SiteWise Portal. +- Create an AWS IoT SiteWise Gateway. +- Describe the AWS IoT SiteWise Gateway. +- Delete the AWS IoT SiteWise Assets. diff --git a/javav2/example_code/redshift/README.md b/javav2/example_code/redshift/README.md index 74823ff8740..08fbc96fca1 100644 --- a/javav2/example_code/redshift/README.md +++ b/javav2/example_code/redshift/README.md @@ -73,8 +73,15 @@ This example shows you how to get started using Amazon Redshift. #### Learn the basics -This example shows you how to learn core operations for Amazon Redshift using an AWS SDK. +This example shows you how to do the following: +- Create a Redshift cluster. +- List databases in the cluster. +- Create a table named Movies. +- Populate the Movies table. +- Query the Movies table by year. +- Modify the Redshift cluster. +- Delete the Amazon Redshift cluster. @@ -110,4 +117,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javav2/example_code/ssm/README.md b/javav2/example_code/ssm/README.md index b27a8dcb3da..a6396b58b0a 100644 --- a/javav2/example_code/ssm/README.md +++ b/javav2/example_code/ssm/README.md @@ -89,8 +89,15 @@ This example shows you how to get started using Systems Manager. #### Learn the basics -This example shows you how to work with Systems Manager maintenance windows, documents, and OpsItems. +This example shows you how to do the following: +- Create a maintenance window. +- Modify the maintenance window schedule. +- Create a document. +- Send a command to a specified EC2 instance. +- Create an OpsItem. +- Update and resolve the OpsItem. +- Delete the maintenance window, OpsItem, and document. @@ -126,4 +133,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/kotlin/services/iot/README.md b/kotlin/services/iot/README.md index a20a8745d52..58a4fb91b24 100644 --- a/kotlin/services/iot/README.md +++ b/kotlin/services/iot/README.md @@ -77,8 +77,19 @@ This example shows you how to get started using AWS IoT. #### Learn the basics -This example shows you how to work with AWS IoT device management. - +This example shows you how to do the following: + +- Create an AWS IoT Thing. +- Generate a device certificate. +- Update an AWS IoT Thing with Attributes. +- Return a unique endpoint. +- List your AWS IoT certificates. +- Create an AWS IoT shadow. +- Write out state information. +- Creates a rule. +- List your rules. +- Search things using the Thing name. +- Delete an AWS IoT Thing. @@ -114,4 +125,4 @@ in the `kotlin` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/iotsitewise/README.md b/python/example_code/iotsitewise/README.md index 7a44db848c8..9c0d4a4adda 100644 --- a/python/example_code/iotsitewise/README.md +++ b/python/example_code/iotsitewise/README.md @@ -86,8 +86,17 @@ python hello/hello_iot_sitewise.py #### Learn the basics -This example shows you how to learn core operations for AWS IoT SiteWise using an AWS SDK. - +This example shows you how to do the following: + +- Create an AWS IoT SiteWise Asset Model. +- Create an AWS IoT SiteWise Asset. +- Retrieve the property ID values. +- Send data to an AWS IoT SiteWise Asset. +- Retrieve the value of the AWS IoT SiteWise Asset property. +- Create an AWS IoT SiteWise Portal. +- Create an AWS IoT SiteWise Gateway. +- Describe the AWS IoT SiteWise Gateway. +- Delete the AWS IoT SiteWise Assets. diff --git a/python/example_code/redshift/README.md b/python/example_code/redshift/README.md index 865bbedb419..dbea462021c 100644 --- a/python/example_code/redshift/README.md +++ b/python/example_code/redshift/README.md @@ -79,8 +79,15 @@ python hello.py #### Learn the basics -This example shows you how to learn core operations for Amazon Redshift using an AWS SDK. +This example shows you how to do the following: +- Create a Redshift cluster. +- List databases in the cluster. +- Create a table named Movies. +- Populate the Movies table. +- Query the Movies table by year. +- Modify the Redshift cluster. +- Delete the Amazon Redshift cluster. @@ -122,4 +129,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/ssm/README.md b/python/example_code/ssm/README.md index cfa07dd0475..588e3a7059a 100644 --- a/python/example_code/ssm/README.md +++ b/python/example_code/ssm/README.md @@ -84,8 +84,15 @@ python hello.py #### Learn the basics -This example shows you how to work with Systems Manager maintenance windows, documents, and OpsItems. +This example shows you how to do the following: +- Create a maintenance window. +- Modify the maintenance window schedule. +- Create a document. +- Send a command to a specified EC2 instance. +- Create an OpsItem. +- Update and resolve the OpsItem. +- Delete the maintenance window, OpsItem, and document. @@ -127,4 +134,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 From 5bb67fb1652f0b808684452a67267b1742fb42c7 Mon Sep 17 00:00:00 2001 From: Dennis Traub Date: Thu, 20 Feb 2025 19:18:44 +0100 Subject: [PATCH 060/144] Python & .NET: Amazon Nova Text & Image Generation (#7250) --- .../metadata/bedrock-runtime_metadata.yaml | 48 +++++++ dotnetv3/Bedrock-runtime/.gitignore | 1 + .../BedrockRuntimeExamples.sln | 31 +++++ .../Converse/Converse.csproj | 4 +- .../InvokeModel/InvokeModel.csproj | 4 +- .../InvokeModel/InvokeModel.cs | 128 ++++++++++++++++++ .../InvokeModel/InvokeModel.csproj | 12 ++ .../AmazonNovaText/Converse/Converse.cs | 60 ++++++++ .../AmazonNovaText/Converse/Converse.csproj | 12 ++ .../ConverseStream/ConverseStream.cs | 67 +++++++++ .../ConverseStream/ConverseStream.csproj | 12 ++ .../AmazonTitanText/Converse/Converse.csproj | 4 +- .../ConverseStream/ConverseStream.csproj | 4 +- .../InvokeModel/InvokeModel.csproj | 4 +- .../InvokeModelWithResponseStream.csproj | 4 +- .../AnthropicClaude/Converse/Converse.csproj | 4 +- .../ConverseStream/ConverseStream.csproj | 4 +- .../InvokeModel/InvokeModel.csproj | 4 +- .../InvokeModelWithResponseStream.csproj | 4 +- .../Command_InvokeModel.csproj | 4 +- ...mmand_InvokeModelWithResponseStream.csproj | 4 +- .../Command_R_InvokeModel.csproj | 4 +- ...and_R_InvokeModelWithResponseStream.csproj | 4 +- .../CohereCommand/Converse/Converse.csproj | 4 +- .../ConverseStream/ConverseStream.csproj | 4 +- .../Models/MetaLlama/Converse/Converse.csproj | 4 +- .../ConverseStream/ConverseStream.csproj | 4 +- .../Llama3_InvokeModel.csproj | 4 +- ...lama3_InvokeModelWithResponseStream.csproj | 4 +- .../Models/Mistral/Converse/Converse.csproj | 4 +- .../ConverseStream/ConverseStream.csproj | 4 +- .../Mistral/InvokeModel/InvokeModel.csproj | 4 +- .../InvokeModelWithResponseStream.csproj | 4 +- dotnetv3/Bedrock-runtime/README.md | 11 +- .../Tests/ActionTest_Converse.cs | 1 + .../Tests/ActionTest_ConverseStream.cs | 1 + .../Tests/ActionTests_InvokeModel.cs | 1 + .../Tests/BedrockRuntimeTests.csproj | 16 ++- python/example_code/bedrock-runtime/README.md | 9 ++ .../amazon_nova_canvas/invoke_model.py | 66 +++++++++ .../amazon_nova/amazon_nova_text/converse.py | 41 ++++++ .../amazon_nova_text/converse_stream.py | 44 ++++++ .../bedrock-runtime/requirements.txt | 37 +---- .../bedrock-runtime/test/test_converse.py | 4 +- .../bedrock-runtime/test/test_invoke_model.py | 4 +- 45 files changed, 615 insertions(+), 87 deletions(-) create mode 100644 dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.cs create mode 100644 dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.csproj create mode 100644 dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.cs create mode 100644 dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.csproj create mode 100644 dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.cs create mode 100644 dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.csproj create mode 100644 python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_canvas/invoke_model.py create mode 100644 python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse.py create mode 100644 python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse_stream.py diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index 2e8de4e4b5e..98b0790e1bc 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -100,6 +100,22 @@ bedrock-runtime_Converse_AmazonNovaText: - description: Send a text message to Amazon Nova, using Bedrock's Converse API. snippet_tags: - bedrock-runtime.java2.Converse_AmazonNovaText + .NET: + versions: + - sdk_version: 3 + github: dotnetv3/Bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova, using Bedrock's Converse API. + snippet_tags: + - BedrockRuntime.dotnetv3.Converse_AmazonNovaText + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova, using Bedrock's Converse API. + snippet_tags: + - python.example_code.bedrock-runtime.Converse_AmazonNovaText services: bedrock-runtime: {Converse} @@ -349,6 +365,22 @@ bedrock-runtime_ConverseStream_AmazonNovaText: - description: Send a text message to Amazon Nova using Bedrock's Converse API and process the response stream in real-time. snippet_tags: - bedrock-runtime.java2.ConverseStream_AmazonNovaText + .NET: + versions: + - sdk_version: 3 + github: dotnetv3/Bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova, using Bedrock's Converse API and process the response stream in real-time. + snippet_tags: + - BedrockRuntime.dotnetv3.ConverseStream_AmazonNovaText + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova, using Bedrock's Converse API and process the response stream in real-time. + snippet_tags: + - python.example_code.bedrock-runtime.ConverseStream_AmazonNovaText services: bedrock-runtime: {ConverseStream} @@ -1137,6 +1169,22 @@ bedrock-runtime_InvokeModel_AmazonNovaImageGeneration: - description: Create an image with Amazon Nova Canvas. snippet_tags: - bedrock-runtime.java2.InvokeModel_AmazonNovaImageGeneration + .NET: + versions: + - sdk_version: 3 + github: dotnetv3/Bedrock-runtime + excerpts: + - description: Create an image with Amazon Nova Canvas. + snippet_tags: + - BedrockRuntime.dotnetv3.InvokeModel_AmazonNovaImageGeneration + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-runtime + excerpts: + - description: Create an image with the Amazon Nova Canvas. + snippet_tags: + - python.example_code.bedrock-runtime.InvokeModel_AmazonNovaImageGeneration services: bedrock-runtime: {InvokeModel} diff --git a/dotnetv3/Bedrock-runtime/.gitignore b/dotnetv3/Bedrock-runtime/.gitignore index ba964e2a8e7..98ae3975334 100644 --- a/dotnetv3/Bedrock-runtime/.gitignore +++ b/dotnetv3/Bedrock-runtime/.gitignore @@ -1,2 +1,3 @@ /.vs/ /Tools/ +**/generated-images/ diff --git a/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln b/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln index f495e60e9b1..d9e5d12e854 100644 --- a/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln +++ b/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln @@ -94,6 +94,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InvokeModelWithResponseStre EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InvokeModelWithResponseStream", "Models\AnthropicClaude\InvokeModelWithResponseStream\InvokeModelWithResponseStream.csproj", "{C75F2BBE-7C84-4B01-9836-7279DAE41499}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNovaText", "AmazonNovaText", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNova", "AmazonNova", "{3AF63EC9-2EB0-4A0B-8C3B-0CA3595080F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converse", "Models\AmazonNova\AmazonNovaText\Converse\Converse.csproj", "{2E4C9BFE-C49C-0567-D73C-F2915AB62CA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConverseStream", "Models\AmazonNova\AmazonNovaText\ConverseStream\ConverseStream.csproj", "{E144492A-337A-0755-EAB4-DA083C3A2DDB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNovaCanvas", "AmazonNovaCanvas", "{4D3E429C-CCAE-42DE-A062-4717E71D8403}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvokeModel", "Models\AmazonNova\AmazonNovaCanvas\InvokeModel\InvokeModel.csproj", "{2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -200,6 +212,18 @@ Global {C75F2BBE-7C84-4B01-9836-7279DAE41499}.Debug|Any CPU.Build.0 = Debug|Any CPU {C75F2BBE-7C84-4B01-9836-7279DAE41499}.Release|Any CPU.ActiveCfg = Release|Any CPU {C75F2BBE-7C84-4B01-9836-7279DAE41499}.Release|Any CPU.Build.0 = Release|Any CPU + {2E4C9BFE-C49C-0567-D73C-F2915AB62CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E4C9BFE-C49C-0567-D73C-F2915AB62CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E4C9BFE-C49C-0567-D73C-F2915AB62CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E4C9BFE-C49C-0567-D73C-F2915AB62CA6}.Release|Any CPU.Build.0 = Release|Any CPU + {E144492A-337A-0755-EAB4-DA083C3A2DDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E144492A-337A-0755-EAB4-DA083C3A2DDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E144492A-337A-0755-EAB4-DA083C3A2DDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E144492A-337A-0755-EAB4-DA083C3A2DDB}.Release|Any CPU.Build.0 = Release|Any CPU + {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -227,6 +251,7 @@ Global {3D6441FC-0FE8-4D0C-910D-3D9310599C71} = {3F96ECB4-1644-43E8-8643-2CDCF9E679F1} {D1B0719F-4F84-4DBC-BCAD-E856FB3193D7} = {8BAC2322-AD3C-484A-B51D-8263BC4E6646} {1E62D4FB-CC59-4F1E-BB22-574CEC08C94B} = {BBB79D3E-5DF2-4FF6-B467-52D0EEB91C4B} + {B753CEB9-EA53-4AE1-997E-B7D54A299D58} = {65504C76-7E32-4A12-A42E-BCDA4FE79BC1} {2A6989CB-B273-4841-BD3E-7B1BBA4DD25F} = {EF45C0B9-ED76-4B7A-A0A7-F102E979B71C} {BCC66C37-4980-484F-819D-066D2FF2669C} = {EF45C0B9-ED76-4B7A-A0A7-F102E979B71C} {52CDA3F4-F090-4224-978A-5F42388DCF92} = {3F96ECB4-1644-43E8-8643-2CDCF9E679F1} @@ -235,6 +260,12 @@ Global {4B5A00D6-B9F1-449F-A9D2-80E860D6BD75} = {65504C76-7E32-4A12-A42E-BCDA4FE79BC1} {EFC7D088-EF45-464B-97CD-0BBA486B224A} = {BBB79D3E-5DF2-4FF6-B467-52D0EEB91C4B} {C75F2BBE-7C84-4B01-9836-7279DAE41499} = {8BAC2322-AD3C-484A-B51D-8263BC4E6646} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3AF63EC9-2EB0-4A0B-8C3B-0CA3595080F6} + {3AF63EC9-2EB0-4A0B-8C3B-0CA3595080F6} = {41B69207-8F29-41BC-9114-78EE740485C8} + {2E4C9BFE-C49C-0567-D73C-F2915AB62CA6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E144492A-337A-0755-EAB4-DA083C3A2DDB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4D3E429C-CCAE-42DE-A062-4717E71D8403} = {3AF63EC9-2EB0-4A0B-8C3B-0CA3595080F6} + {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D} = {4D3E429C-CCAE-42DE-A062-4717E71D8403} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E48A5088-1BBB-4A8B-9AB2-CC5CE0482466} diff --git a/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/Converse/Converse.csproj index 8475494e76e..c26f412667b 100644 --- a/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/Converse/Converse.csproj +++ b/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/Converse/Converse.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/InvokeModel/InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/InvokeModel/InvokeModel.csproj index bf2403af903..986018da574 100644 --- a/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/InvokeModel/InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/Ai21LabsJurassic2/InvokeModel/InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.cs b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.cs new file mode 100644 index 00000000000..6db2f957aef --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.cs @@ -0,0 +1,128 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[BedrockRuntime.dotnetv3.InvokeModel_AmazonNovaImageGeneration] +// Use the native inference API to create an image with Amazon Nova Canvas. + +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using Amazon; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; + +// Create a Bedrock Runtime client in the AWS Region you want to use. +var client = new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1); + +// Set the model ID. +var modelId = "amazon.nova-canvas-v1:0"; + +// Define the image generation prompt for the model. +var prompt = "A stylized picture of a cute old steampunk robot."; + +// Create a random seed between 0 and 858,993,459 +int seed = new Random().Next(0, 858993460); + +//Format the request payload using the model's native structure. +var nativeRequest = JsonSerializer.Serialize(new +{ + taskType = "TEXT_IMAGE", + textToImageParams = new + { + text = prompt + }, + imageGenerationConfig = new + { + seed, + quality = "standard", + width = 512, + height = 512, + numberOfImages = 1 + } +}); + +// Create a request with the model ID and the model's native request payload. +var request = new InvokeModelRequest() +{ + ModelId = modelId, + Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(nativeRequest)), + ContentType = "application/json" +}; + +try +{ + // Send the request to the Bedrock Runtime and wait for the response. + var response = await client.InvokeModelAsync(request); + + // Decode the response body. + var modelResponse = await JsonNode.ParseAsync(response.Body); + + // Extract the image data. + var base64Image = modelResponse["images"]?[0].ToString() ?? ""; + + // Save the image in a local folder + string savedPath = AmazonNovaCanvas.InvokeModel.SaveBase64Image(base64Image); + Console.WriteLine($"Image saved to: {savedPath}"); +} +catch (AmazonBedrockRuntimeException e) +{ + Console.WriteLine($"ERROR: Can't invoke '{modelId}'. Reason: {e.Message}"); + throw; +} + +// snippet-end:[BedrockRuntime.dotnetv3.InvokeModel_AmazonNovaImageGeneration] + +// Create a partial class to make the top-level script testable. +namespace AmazonNovaCanvas +{ + public partial class InvokeModel + { + public static string SaveBase64Image(string base64String, string outputFolderName = "generated-images") + { + // Get the directory where the script is located + string scriptDirectory = AppDomain.CurrentDomain.BaseDirectory; + + // Navigate to the script's folder + if (scriptDirectory.Contains("bin")) + { + scriptDirectory = Directory.GetParent(scriptDirectory)?.Parent?.Parent?.Parent?.FullName + ?? throw new DirectoryNotFoundException("Could not find script directory"); + } + + // Combine script directory with output folder + string outputPath = Path.Combine(scriptDirectory, outputFolderName); + + // Create directory if it doesn't exist + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + + // Remove base64 header if present (e.g., "data:image/jpeg;base64,") + string base64Data = base64String; + if (base64String.Contains(",")) + { + base64Data = base64String.Split(',')[1]; + } + + // Convert base64 to bytes + byte[] imageBytes = Convert.FromBase64String(base64Data); + + // Find the next available number + int fileNumber = 1; + string filePath; + do + { + string paddedNumber = fileNumber.ToString("D2"); // Pads with leading zero + filePath = Path.Combine(outputPath, $"image_{paddedNumber}.jpg"); + fileNumber++; + } while (File.Exists(filePath)); + + // Save the image + File.WriteAllBytes(filePath, imageBytes); + + return filePath; + } + } +} \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.csproj new file mode 100644 index 00000000000..0db5411af0b --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + AmazonNovaCanvas.$(MSBuildProjectName) + + + + + + + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.cs b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.cs new file mode 100644 index 00000000000..46466fec1d2 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.cs @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[BedrockRuntime.dotnetv3.Converse_AmazonNovaText] +// Use the Converse API to send a text message to Amazon Nova. + +using System; +using System.Collections.Generic; +using Amazon; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; + +// Create a Bedrock Runtime client in the AWS Region you want to use. +var client = new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1); + +// Set the model ID, e.g., Amazon Nova Lite. +var modelId = "amazon.nova-lite-v1:0"; + +// Define the user message. +var userMessage = "Describe the purpose of a 'hello world' program in one line."; + +// Create a request with the model ID, the user message, and an inference configuration. +var request = new ConverseRequest +{ + ModelId = modelId, + Messages = new List + { + new Message + { + Role = ConversationRole.User, + Content = new List { new ContentBlock { Text = userMessage } } + } + }, + InferenceConfig = new InferenceConfiguration() + { + MaxTokens = 512, + Temperature = 0.5F, + TopP = 0.9F + } +}; + +try +{ + // Send the request to the Bedrock Runtime and wait for the result. + var response = await client.ConverseAsync(request); + + // Extract and print the response text. + string responseText = response?.Output?.Message?.Content?[0]?.Text ?? ""; + Console.WriteLine(responseText); +} +catch (AmazonBedrockRuntimeException e) +{ + Console.WriteLine($"ERROR: Can't invoke '{modelId}'. Reason: {e.Message}"); + throw; +} + +// snippet-end:[BedrockRuntime.dotnetv3.Converse_AmazonNovaText] + +// Create a partial class to make the top-level script testable. +namespace AmazonNovaText { public partial class Converse { } } \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.csproj new file mode 100644 index 00000000000..5fa769392db --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/Converse/Converse.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + AmazonNovaText.$(MSBuildProjectName) + + + + + + + diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.cs b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.cs new file mode 100644 index 00000000000..69ff7825dd9 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.cs @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[BedrockRuntime.dotnetv3.ConverseStream_AmazonNovaText] +// Use the Converse API to send a text message to Amazon Nova +// and print the response stream. + +using System; +using System.Collections.Generic; +using System.Linq; +using Amazon; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; + +// Create a Bedrock Runtime client in the AWS Region you want to use. +var client = new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1); + +// Set the model ID, e.g., Amazon Nova Lite. +var modelId = "amazon.nova-lite-v1:0"; + +// Define the user message. +var userMessage = "Describe the purpose of a 'hello world' program in one line."; + +// Create a request with the model ID, the user message, and an inference configuration. +var request = new ConverseStreamRequest +{ + ModelId = modelId, + Messages = new List + { + new Message + { + Role = ConversationRole.User, + Content = new List { new ContentBlock { Text = userMessage } } + } + }, + InferenceConfig = new InferenceConfiguration() + { + MaxTokens = 512, + Temperature = 0.5F, + TopP = 0.9F + } +}; + +try +{ + // Send the request to the Bedrock Runtime and wait for the result. + var response = await client.ConverseStreamAsync(request); + + // Extract and print the streamed response text in real-time. + foreach (var chunk in response.Stream.AsEnumerable()) + { + if (chunk is ContentBlockDeltaEvent) + { + Console.Write((chunk as ContentBlockDeltaEvent).Delta.Text); + } + } +} +catch (AmazonBedrockRuntimeException e) +{ + Console.WriteLine($"ERROR: Can't invoke '{modelId}'. Reason: {e.Message}"); + throw; +} + +// snippet-end:[BedrockRuntime.dotnetv3.ConverseStream_AmazonNovaText] + +// Create a partial class to make the top-level script testable. +namespace AmazonNovaText { public partial class ConverseStream { } } \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.csproj new file mode 100644 index 00000000000..1260dc1d435 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + AmazonNovaText.$(MSBuildProjectName) + + + + + + + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/Converse/Converse.csproj index e505af96607..3651f4be200 100644 --- a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/Converse/Converse.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/Converse/Converse.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/ConverseStream/ConverseStream.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/ConverseStream/ConverseStream.csproj index 5752f31c880..662bf35f3e1 100644 --- a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/ConverseStream/ConverseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/ConverseStream/ConverseStream.csproj @@ -6,7 +6,7 @@ - - + + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModel/InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModel/InvokeModel.csproj index 5752f31c880..662bf35f3e1 100644 --- a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModel/InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModel/InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj index 5752f31c880..662bf35f3e1 100644 --- a/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AmazonTitanText/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj @@ -6,7 +6,7 @@ - - + + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/Converse/Converse.csproj index 7f752984648..9a843cc1582 100644 --- a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/Converse/Converse.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/Converse/Converse.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/ConverseStream/ConverseStream.csproj b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/ConverseStream/ConverseStream.csproj index e4e6c3bb250..72b5e19f0b8 100644 --- a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/ConverseStream/ConverseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/ConverseStream/ConverseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModel/InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModel/InvokeModel.csproj index e4e6c3bb250..72b5e19f0b8 100644 --- a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModel/InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModel/InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj index 0daad35f8b9..6e4dbdf489b 100644 --- a/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/AnthropicClaude/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj @@ -6,7 +6,7 @@ - - + + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModel/Command_InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModel/Command_InvokeModel.csproj index 402f8c682cb..b79f0fd7312 100644 --- a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModel/Command_InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModel/Command_InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModelWithResponseStream/Command_InvokeModelWithResponseStream.csproj b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModelWithResponseStream/Command_InvokeModelWithResponseStream.csproj index 402f8c682cb..b79f0fd7312 100644 --- a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModelWithResponseStream/Command_InvokeModelWithResponseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_InvokeModelWithResponseStream/Command_InvokeModelWithResponseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModel/Command_R_InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModel/Command_R_InvokeModel.csproj index 402f8c682cb..b79f0fd7312 100644 --- a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModel/Command_R_InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModel/Command_R_InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModelWithResponseStream/Command_R_InvokeModelWithResponseStream.csproj b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModelWithResponseStream/Command_R_InvokeModelWithResponseStream.csproj index 402f8c682cb..b79f0fd7312 100644 --- a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModelWithResponseStream/Command_R_InvokeModelWithResponseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Command_R_InvokeModelWithResponseStream/Command_R_InvokeModelWithResponseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Converse/Converse.csproj index 402f8c682cb..b79f0fd7312 100644 --- a/dotnetv3/Bedrock-runtime/Models/CohereCommand/Converse/Converse.csproj +++ b/dotnetv3/Bedrock-runtime/Models/CohereCommand/Converse/Converse.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/CohereCommand/ConverseStream/ConverseStream.csproj b/dotnetv3/Bedrock-runtime/Models/CohereCommand/ConverseStream/ConverseStream.csproj index 402f8c682cb..b79f0fd7312 100644 --- a/dotnetv3/Bedrock-runtime/Models/CohereCommand/ConverseStream/ConverseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/CohereCommand/ConverseStream/ConverseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/MetaLlama/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/MetaLlama/Converse/Converse.csproj index f91317c7fa6..6163a7486a7 100644 --- a/dotnetv3/Bedrock-runtime/Models/MetaLlama/Converse/Converse.csproj +++ b/dotnetv3/Bedrock-runtime/Models/MetaLlama/Converse/Converse.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/MetaLlama/ConverseStream/ConverseStream.csproj b/dotnetv3/Bedrock-runtime/Models/MetaLlama/ConverseStream/ConverseStream.csproj index f91317c7fa6..6163a7486a7 100644 --- a/dotnetv3/Bedrock-runtime/Models/MetaLlama/ConverseStream/ConverseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/MetaLlama/ConverseStream/ConverseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModel/Llama3_InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModel/Llama3_InvokeModel.csproj index f91317c7fa6..6163a7486a7 100644 --- a/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModel/Llama3_InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModel/Llama3_InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModelWithResponseStream/Llama3_InvokeModelWithResponseStream.csproj b/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModelWithResponseStream/Llama3_InvokeModelWithResponseStream.csproj index f91317c7fa6..6163a7486a7 100644 --- a/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModelWithResponseStream/Llama3_InvokeModelWithResponseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/MetaLlama/Llama3_InvokeModelWithResponseStream/Llama3_InvokeModelWithResponseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/Mistral/Converse/Converse.csproj b/dotnetv3/Bedrock-runtime/Models/Mistral/Converse/Converse.csproj index 27e936ccbc6..dfbd70d9fc3 100644 --- a/dotnetv3/Bedrock-runtime/Models/Mistral/Converse/Converse.csproj +++ b/dotnetv3/Bedrock-runtime/Models/Mistral/Converse/Converse.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/Mistral/ConverseStream/ConverseStream.csproj b/dotnetv3/Bedrock-runtime/Models/Mistral/ConverseStream/ConverseStream.csproj index 8297baab449..9f570ade55e 100644 --- a/dotnetv3/Bedrock-runtime/Models/Mistral/ConverseStream/ConverseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/Mistral/ConverseStream/ConverseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModel/InvokeModel.csproj b/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModel/InvokeModel.csproj index 8297baab449..9f570ade55e 100644 --- a/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModel/InvokeModel.csproj +++ b/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModel/InvokeModel.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj b/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj index 8297baab449..9f570ade55e 100644 --- a/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj +++ b/dotnetv3/Bedrock-runtime/Models/Mistral/InvokeModelWithResponseStream/InvokeModelWithResponseStream.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/dotnetv3/Bedrock-runtime/README.md b/dotnetv3/Bedrock-runtime/README.md index 9e153544bb9..a51ea913fdc 100644 --- a/dotnetv3/Bedrock-runtime/README.md +++ b/dotnetv3/Bedrock-runtime/README.md @@ -33,6 +33,15 @@ For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv3 - [Converse](Models/Ai21LabsJurassic2/Converse/Converse.cs#L4) - [InvokeModel](Models/Ai21LabsJurassic2/InvokeModel/InvokeModel.cs#L4) +### Amazon Nova + +- [Converse](Models/AmazonNova/AmazonNovaText/Converse/Converse.cs#L4) +- [ConverseStream](Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.cs#L4) + +### Amazon Nova Canvas + +- [InvokeModel](Models/AmazonNova/AmazonNovaCanvas/InvokeModel/InvokeModel.cs#L4) + ### Amazon Titan Text - [Converse](Models/AmazonTitanText/Converse/Converse.cs#L4) @@ -127,4 +136,4 @@ in the `dotnetv3` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv3/Bedrock-runtime/Tests/ActionTest_Converse.cs b/dotnetv3/Bedrock-runtime/Tests/ActionTest_Converse.cs index d57db96634e..bb3fafc643d 100644 --- a/dotnetv3/Bedrock-runtime/Tests/ActionTest_Converse.cs +++ b/dotnetv3/Bedrock-runtime/Tests/ActionTest_Converse.cs @@ -10,6 +10,7 @@ public class ActionTest_Converse [InlineData(typeof(MetaLlama.Converse))] [InlineData(typeof(CohereCommand.Converse))] [InlineData(typeof(AnthropicClaude.Converse))] + [InlineData(typeof(AmazonNovaText.Converse))] [InlineData(typeof(AmazonTitanText.Converse))] [InlineData(typeof(Ai21LabsJurassic2.Converse))] public void ConverseDoesNotThrow(Type type) diff --git a/dotnetv3/Bedrock-runtime/Tests/ActionTest_ConverseStream.cs b/dotnetv3/Bedrock-runtime/Tests/ActionTest_ConverseStream.cs index 3c4ab3417f0..0f6ca41ccac 100644 --- a/dotnetv3/Bedrock-runtime/Tests/ActionTest_ConverseStream.cs +++ b/dotnetv3/Bedrock-runtime/Tests/ActionTest_ConverseStream.cs @@ -10,6 +10,7 @@ public class ActionTest_ConverseStream [InlineData(typeof(MetaLlama.ConverseStream))] [InlineData(typeof(CohereCommand.ConverseStream))] [InlineData(typeof(AnthropicClaude.ConverseStream))] + [InlineData(typeof(AmazonNovaText.ConverseStream))] [InlineData(typeof(AmazonTitanText.ConverseStream))] public void ConverseStreamDoesNotThrow(Type type) { diff --git a/dotnetv3/Bedrock-runtime/Tests/ActionTests_InvokeModel.cs b/dotnetv3/Bedrock-runtime/Tests/ActionTests_InvokeModel.cs index 0584cf61793..0b561dc2176 100644 --- a/dotnetv3/Bedrock-runtime/Tests/ActionTests_InvokeModel.cs +++ b/dotnetv3/Bedrock-runtime/Tests/ActionTests_InvokeModel.cs @@ -13,6 +13,7 @@ public class ActionTest_InvokeModel [InlineData(typeof(AnthropicClaude.InvokeModel))] [InlineData(typeof(AmazonTitanText.InvokeModel))] [InlineData(typeof(Ai21LabsJurassic2.InvokeModel))] + [InlineData(typeof(AmazonNovaCanvas.InvokeModel))] public void InvokeModelDoesNotThrow(Type type) { var entryPoint = type.Assembly.EntryPoint!; diff --git a/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj b/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj index b499eb4f7a0..ef74e91feaf 100644 --- a/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj +++ b/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj @@ -11,16 +11,16 @@ - - - - + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -29,6 +29,10 @@ + + + + diff --git a/python/example_code/bedrock-runtime/README.md b/python/example_code/bedrock-runtime/README.md index dce1178412d..91bca8633dc 100644 --- a/python/example_code/bedrock-runtime/README.md +++ b/python/example_code/bedrock-runtime/README.md @@ -48,6 +48,15 @@ python -m pip install -r requirements.txt - [Converse](models/ai21_labs_jurassic2/converse.py#L4) - [InvokeModel](models/ai21_labs_jurassic2/invoke_model.py#L4) +### Amazon Nova + +- [Converse](models/amazon_nova/amazon_nova_text/converse.py#L4) +- [ConverseStream](models/amazon_nova/amazon_nova_text/converse_stream.py#L4) + +### Amazon Nova Canvas + +- [InvokeModel](models/amazon_nova/amazon_nova_canvas/invoke_model.py#L4) + ### Amazon Titan Image Generator - [InvokeModel](models/amazon_titan_image_generator/invoke_model.py#L4) diff --git a/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_canvas/invoke_model.py b/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_canvas/invoke_model.py new file mode 100644 index 00000000000..0e479fbf832 --- /dev/null +++ b/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_canvas/invoke_model.py @@ -0,0 +1,66 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[python.example_code.bedrock-runtime.InvokeModel_AmazonNovaImageGeneration] +# Use the native inference API to create an image with Amazon Nova Canvas + +import base64 +import json +import os +import random + +import boto3 + +# Create a Bedrock Runtime client in the AWS Region of your choice. +client = boto3.client("bedrock-runtime", region_name="us-east-1") + +# Set the model ID. +model_id = "amazon.nova-canvas-v1:0" + +# Define the image generation prompt for the model. +prompt = "A stylized picture of a cute old steampunk robot." + +# Generate a random seed between 0 and 858,993,459 +seed = random.randint(0, 858993460) + +# Format the request payload using the model's native structure. +native_request = { + "taskType": "TEXT_IMAGE", + "textToImageParams": {"text": prompt}, + "imageGenerationConfig": { + "seed": seed, + "quality": "standard", + "height": 512, + "width": 512, + "numberOfImages": 1, + }, +} + +# Convert the native request to JSON. +request = json.dumps(native_request) + +# Invoke the model with the request. +response = client.invoke_model(modelId=model_id, body=request) + +# Decode the response body. +model_response = json.loads(response["body"].read()) + +# Extract the image data. +base64_image_data = model_response["images"][0] + +# Save the generated image to a local folder. +i, output_dir = 1, "output" +if not os.path.exists(output_dir): + os.makedirs(output_dir) +while os.path.exists(os.path.join(output_dir, f"nova_canvas_{i}.png")): + i += 1 + +image_data = base64.b64decode(base64_image_data) + +image_path = os.path.join(output_dir, f"nova_canvas_{i}.png") +with open(image_path, "wb") as file: + file.write(image_data) + +print(f"The generated image has been saved to {image_path}") + +# snippet-end:[python.example_code.bedrock-runtime.InvokeModel_AmazonNovaImageGeneration] diff --git a/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse.py b/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse.py new file mode 100644 index 00000000000..3000a6a62f2 --- /dev/null +++ b/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse.py @@ -0,0 +1,41 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[python.example_code.bedrock-runtime.Converse_AmazonNovaText] +# Use the Conversation API to send a text message to Amazon Nova. + +import boto3 +from botocore.exceptions import ClientError + +# Create a Bedrock Runtime client in the AWS Region you want to use. +client = boto3.client("bedrock-runtime", region_name="us-east-1") + +# Set the model ID, e.g., Amazon Nova Lite. +model_id = "amazon.nova-lite-v1:0" + +# Start a conversation with the user message. +user_message = "Describe the purpose of a 'hello world' program in one line." +conversation = [ + { + "role": "user", + "content": [{"text": user_message}], + } +] + +try: + # Send the message to the model, using a basic inference configuration. + response = client.converse( + modelId=model_id, + messages=conversation, + inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9}, + ) + + # Extract and print the response text. + response_text = response["output"]["message"]["content"][0]["text"] + print(response_text) + +except (ClientError, Exception) as e: + print(f"ERROR: Can't invoke '{model_id}'. Reason: {e}") + exit(1) + +# snippet-end:[python.example_code.bedrock-runtime.Converse_AmazonNovaText] diff --git a/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse_stream.py b/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse_stream.py new file mode 100644 index 00000000000..a71bacf375f --- /dev/null +++ b/python/example_code/bedrock-runtime/models/amazon_nova/amazon_nova_text/converse_stream.py @@ -0,0 +1,44 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[python.example_code.bedrock-runtime.ConverseStream_AmazonNovaText] +# Use the Conversation API to send a text message to Amazon Nova Text +# and print the response stream. + +import boto3 +from botocore.exceptions import ClientError + +# Create a Bedrock Runtime client in the AWS Region you want to use. +client = boto3.client("bedrock-runtime", region_name="us-east-1") + +# Set the model ID, e.g., Amazon Nova Lite. +model_id = "amazon.nova-lite-v1:0" + +# Start a conversation with the user message. +user_message = "Describe the purpose of a 'hello world' program in one line." +conversation = [ + { + "role": "user", + "content": [{"text": user_message}], + } +] + +try: + # Send the message to the model, using a basic inference configuration. + streaming_response = client.converse_stream( + modelId=model_id, + messages=conversation, + inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9}, + ) + + # Extract and print the streamed response text in real-time. + for chunk in streaming_response["stream"]: + if "contentBlockDelta" in chunk: + text = chunk["contentBlockDelta"]["delta"]["text"] + print(text, end="") + +except (ClientError, Exception) as e: + print(f"ERROR: Can't invoke '{model_id}'. Reason: {e}") + exit(1) + +# snippet-end:[python.example_code.bedrock-runtime.ConverseStream_AmazonNovaText] diff --git a/python/example_code/bedrock-runtime/requirements.txt b/python/example_code/bedrock-runtime/requirements.txt index e084f5d893a..2e935f7aa85 100644 --- a/python/example_code/bedrock-runtime/requirements.txt +++ b/python/example_code/bedrock-runtime/requirements.txt @@ -1,35 +1,12 @@ -beautifulsoup4==4.12.3 -boto3==1.35.28 -botocore==1.35.28 -certifi==2024.8.30 -charset-normalizer==3.3.2 +boto3==1.36.22 +botocore==1.36.22 colorama==0.4.6 -contourpy==1.3.0 -cycler==0.12.1 -fonttools==4.54.1 -geojson==3.1.0 -idna==3.10 iniconfig==2.0.0 jmespath==1.0.1 -kiwisolver==1.4.7 -lxml==5.3.0 -matplotlib==3.9.2 -numpy==2.1.1 -packaging==24.1 -pandas==2.2.3 -pillow==10.4.0 -pip-review==1.3.0 +packaging==24.2 pluggy==1.5.0 -pyparsing==3.1.4 -pytest==8.3.3 -pytest-asyncio==0.24.0 +pytest==8.3.4 python-dateutil==2.9.0.post0 -pytz==2024.2 -requests==2.32.3 -s3transfer==0.10.2 -six==1.16.0 -soupsieve==2.6 -tzdata==2024.2 -ujson==5.10.0 -urllib3==2.2.3 -xarray==2024.9.0 +s3transfer==0.11.2 +six==1.17.0 +urllib3==2.3.0 diff --git a/python/example_code/bedrock-runtime/test/test_converse.py b/python/example_code/bedrock-runtime/test/test_converse.py index a24b9c2fd42..4bcefb110ae 100644 --- a/python/example_code/bedrock-runtime/test/test_converse.py +++ b/python/example_code/bedrock-runtime/test/test_converse.py @@ -1,12 +1,14 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest import subprocess import sys +import pytest + files_under_test = [ "models/ai21_labs_jurassic2/converse.py", + "models/amazon_nova/amazon_nova_text/converse.py", "models/amazon_titan_text/converse.py", "models/anthropic_claude/converse.py", "models/cohere_command/converse.py", diff --git a/python/example_code/bedrock-runtime/test/test_invoke_model.py b/python/example_code/bedrock-runtime/test/test_invoke_model.py index 1193de8d162..562ad785d8e 100644 --- a/python/example_code/bedrock-runtime/test/test_invoke_model.py +++ b/python/example_code/bedrock-runtime/test/test_invoke_model.py @@ -1,13 +1,15 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest import subprocess import sys +import pytest + files_under_test = [ # Text models "models/ai21_labs_jurassic2/invoke_model.py", + "models/amazon_nova/amazon_nova_canvas/invoke_model.py", "models/amazon_titan_text/invoke_model.py", "models/anthropic_claude/invoke_model.py", "models/cohere_command/command_invoke_model.py", From 8f7f791226ddfb8598d5158b1768c5cbce9a5c4b Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:42:27 -0600 Subject: [PATCH 061/144] Fixes for GitHub Actions to use Feature Scenario label. (#7254) --- .github/allowed-labels.yml | 2 +- .github/workflows/label-checker.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/allowed-labels.yml b/.github/allowed-labels.yml index 2dd39429d1c..f6d223588a0 100644 --- a/.github/allowed-labels.yml +++ b/.github/allowed-labels.yml @@ -12,7 +12,7 @@ - name: MVP color: f5f7f9 description: "A Minimum Viable Product example to show the bare bones of how to use a service via an SDK." -- name: Workflow +- name: Feature Scenario color: f5f7f9 description: "A simple code example to show how certain tasks can be accomplished using several services and SDKs." - name: Basics diff --git a/.github/workflows/label-checker.yml b/.github/workflows/label-checker.yml index 6d26deb93d8..3c3893ecef9 100644 --- a/.github/workflows/label-checker.yml +++ b/.github/workflows/label-checker.yml @@ -26,5 +26,5 @@ jobs: steps: - uses: docker://agilepathway/pull-request-label-checker:latest with: - one_of: Application,MVP,Workflow,Task,Bug,Basics + one_of: Application,MVP,Feature Scenario,Task,Bug,Basics repo_token: ${{ secrets.GITHUB_TOKEN }} From ae2207e4612cbaae91c1c5d8bcc64dbfe9f74d63 Mon Sep 17 00:00:00 2001 From: Antonio Masotti Date: Thu, 20 Feb 2025 20:31:22 +0100 Subject: [PATCH 062/144] Kotlin: Add Bedrock Runtime invoke model example (#7221) Co-authored-by: scmacdon --- .../metadata/bedrock-runtime_metadata.yaml | 8 ++ kotlin/services/bedrock-runtime/README.md | 77 +++++++++++++++++++ .../services/bedrock-runtime/build.gradle.kts | 54 +++++++++++++ .../com/example/bedrockruntime/InvokeModel.kt | 67 ++++++++++++++++ .../src/test/kotlin/InvokeModelTest.kt | 24 ++++++ 5 files changed, 230 insertions(+) create mode 100644 kotlin/services/bedrock-runtime/README.md create mode 100644 kotlin/services/bedrock-runtime/build.gradle.kts create mode 100644 kotlin/services/bedrock-runtime/src/main/kotlin/com/example/bedrockruntime/InvokeModel.kt create mode 100644 kotlin/services/bedrock-runtime/src/test/kotlin/InvokeModelTest.kt diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index 98b0790e1bc..d9433b2dcbd 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -673,6 +673,14 @@ bedrock-runtime_InvokeModel_TitanText: - description: Use the Invoke Model API to send a text message. snippet_tags: - bedrock-runtime.java2.InvokeModel_AmazonTitanText + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/bedrock-runtime + excerpts: + - description: Use the Invoke Model API to generate a short story. + snippet_tags: + - bedrock-runtime.kotlin.InvokeModel_AmazonTitanText .NET: versions: - sdk_version: 3 diff --git a/kotlin/services/bedrock-runtime/README.md b/kotlin/services/bedrock-runtime/README.md new file mode 100644 index 00000000000..2d5be2d977c --- /dev/null +++ b/kotlin/services/bedrock-runtime/README.md @@ -0,0 +1,77 @@ +# Amazon Bedrock Runtime code examples for the SDK for Kotlin + +## Overview + +Shows how to use the AWS SDK for Kotlin to work with Amazon Bedrock Runtime. + + +This section provides examples that show how to invoke foundation models using the Amazon Bedrock Runtime API with the AWS SDK for Kotlin. + + +_Amazon Bedrock Runtime is a fully managed service that makes it easy to use foundation models from third-party providers and Amazon._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `kotlin` folder. + + + +> ⚠ You must request access to a model before you can use it. If you try to use the model (with the API or console) before you have requested access to it, you will receive an error message. For more information, see [Model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). + +### Amazon Titan Text + +- [InvokeModel](src/main/kotlin/com/example/bedrockruntime/InvokeModel.kt#L6) + + + + + +## Run the examples + +### Instructions + + + + + + + +### 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 `kotlin` folder. + + + + + + +## Additional resources + +- [Amazon Bedrock Runtime User Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) +- [Amazon Bedrock Runtime API Reference](https://docs.aws.amazon.com/bedrock/latest/APIReference/welcome.html) +- [SDK for Kotlin Amazon Bedrock Runtime reference](https://sdk.amazonaws.com/kotlin/api/latest/bedrock-runtime/index.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/kotlin/services/bedrock-runtime/build.gradle.kts b/kotlin/services/bedrock-runtime/build.gradle.kts new file mode 100644 index 00000000000..b51ca6849ff --- /dev/null +++ b/kotlin/services/bedrock-runtime/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + kotlin("jvm") version "2.1.10" + id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" + id("org.jlleitschuh.gradle.ktlint") version "11.3.1" apply true + application +} + +group = "com.example.bedrockruntime" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +buildscript { + repositories { + maven("https://plugins.gradle.org/m2/") + } + dependencies { + classpath("org.jlleitschuh.gradle:ktlint-gradle:11.3.1") + } +} + +dependencies { + implementation("aws.sdk.kotlin:bedrockruntime:1.4.11") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.8.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") +} + +application { + mainClass.set("com.example.bedrockruntime.InvokeModelKt") +} + +// Java and Kotlin configuration +kotlin { + jvmToolchain(21) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + + // Define the test source set + testClassesDirs += files("build/classes/kotlin/test") + classpath += files("build/classes/kotlin/main", "build/resources/main") +} diff --git a/kotlin/services/bedrock-runtime/src/main/kotlin/com/example/bedrockruntime/InvokeModel.kt b/kotlin/services/bedrock-runtime/src/main/kotlin/com/example/bedrockruntime/InvokeModel.kt new file mode 100644 index 00000000000..167bccac5b0 --- /dev/null +++ b/kotlin/services/bedrock-runtime/src/main/kotlin/com/example/bedrockruntime/InvokeModel.kt @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime + +// snippet-start:[bedrock-runtime.kotlin.InvokeModel_AmazonTitanText] +import aws.sdk.kotlin.services.bedrockruntime.BedrockRuntimeClient +import aws.sdk.kotlin.services.bedrockruntime.model.InvokeModelRequest +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** + * Before running this Kotlin code example, set up your development environment, including your credentials. + * + * This example demonstrates how to invoke the Titan Text model (amazon.titan-text-lite-v1). + * Remember that you must enable the model before you can use it. See notes in the README.md file. + * + * For more information, see the following documentation topic: + * https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/setup.html + */ +suspend fun main() { + val prompt = """ + Write a short, funny story about a time-traveling cat who + ends up in ancient Egypt at the time of the pyramids. + """.trimIndent() + + val response = invokeModel(prompt, "amazon.titan-text-lite-v1") + println("Generated story:\n$response") +} + +suspend fun invokeModel(prompt: String, modelId: String): String { + BedrockRuntimeClient { region = "eu-central-1" }.use { client -> + val request = InvokeModelRequest { + this.modelId = modelId + contentType = "application/json" + accept = "application/json" + body = """ + { + "inputText": "${prompt.replace(Regex("\\s+"), " ").trim()}", + "textGenerationConfig": { + "maxTokenCount": 1000, + "stopSequences": [], + "temperature": 1, + "topP": 0.7 + } + } + """.trimIndent().toByteArray() + } + + val response = client.invokeModel(request) + val responseBody = response.body.toString(Charsets.UTF_8) + + val jsonParser = Json { ignoreUnknownKeys = true } + return jsonParser + .decodeFromString(responseBody) + .results + .first() + .outputText + } +} + +@Serializable +private data class BedrockResponse(val results: List) + +@Serializable +private data class Result(val outputText: String) +// snippet-end:[bedrock-runtime.kotlin.InvokeModel_AmazonTitanText] diff --git a/kotlin/services/bedrock-runtime/src/test/kotlin/InvokeModelTest.kt b/kotlin/services/bedrock-runtime/src/test/kotlin/InvokeModelTest.kt new file mode 100644 index 00000000000..21d6eb43eb1 --- /dev/null +++ b/kotlin/services/bedrock-runtime/src/test/kotlin/InvokeModelTest.kt @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import com.example.bedrockruntime.invokeModel +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestMethodOrder + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(OrderAnnotation::class) +class InvokeModelTest { + @Test + @Order(1) + fun listFoundationModels() = runBlocking { + val prompt = "What is the capital of France?" + + val answer = invokeModel(prompt, "amazon.titan-text-lite-v1") + assertTrue(answer.isNotBlank()) + } +} From 103927a3358876ab04cc02e75e8c7c91fbbc51de Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:28:20 -0600 Subject: [PATCH 063/144] .NET v3: Bedrock Converse API with Tool Feature Scenario Scout (#7229) --- .../metadata/bedrock-runtime_metadata.yaml | 85 ++++- .../BedrockRuntimeExamples.sln | 18 + dotnetv3/Bedrock-runtime/README.md | 20 + .../BedrockActionsWrapper.cs | 82 ++++ .../ConverseToolScenario.cs | 361 ++++++++++++++++++ .../ConverseToolScenario.csproj | 19 + .../Scenarios/ConverseToolScenario/README.md | 59 +++ .../ConverseToolScenario/ToolResponse.cs | 16 + .../ConverseToolScenario/WeatherTool.cs | 98 +++++ .../Tests/BedrockRuntimeTests.csproj | 3 +- .../Tests/ConverseToolScenarioTests.cs | 65 ++++ .../Bedrock-runtime/Tests/GlobalUsings.cs | 7 +- dotnetv3/DotNetV3Examples.sln | 7 + .../Bedrock/Actions/BedrockActions.csproj | 1 + python/example_code/bedrock-runtime/README.md | 25 ++ rustv1/examples/bedrock-runtime/README.md | 21 +- .../features/bedrock_converse_tool/README.md | 42 ++ .../bedrock_converse_tool/SPECIFICATION.md | 246 ++++++++++++ .../bedrock_converse_tool/toolscenario.png | Bin 0 -> 40313 bytes 19 files changed, 1166 insertions(+), 9 deletions(-) create mode 100644 dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/BedrockActionsWrapper.cs create mode 100644 dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.cs create mode 100644 dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.csproj create mode 100644 dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/README.md create mode 100644 dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ToolResponse.cs create mode 100644 dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/WeatherTool.cs create mode 100644 dotnetv3/Bedrock-runtime/Tests/ConverseToolScenarioTests.cs create mode 100644 scenarios/features/bedrock_converse_tool/README.md create mode 100644 scenarios/features/bedrock_converse_tool/SPECIFICATION.md create mode 100644 scenarios/features/bedrock_converse_tool/toolscenario.png diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index d9433b2dcbd..e0c702bd667 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -108,6 +108,10 @@ bedrock-runtime_Converse_AmazonNovaText: - description: Send a text message to Amazon Nova, using Bedrock's Converse API. snippet_tags: - BedrockRuntime.dotnetv3.Converse_AmazonNovaText + - description: Send a conversation of messages to Amazon Nova using Bedrock's Converse API with a tool configuration. + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.SendConverseRequest Python: versions: - sdk_version: 3 @@ -163,6 +167,60 @@ bedrock-runtime_Converse_AmazonTitanText: services: bedrock-runtime: {Converse} +bedrock-runtime_Scenario_ToolUse: + title: "A tool use example illustrating how to connect AI models on &BR; with a custom tool or API" + title_abbrev: "Tool use with the Converse API" + synopsis: "build a typical interaction between an application, a generative AI model, and connected tools or APIs to mediate interactions between the AI and the outside world. It uses the example of connecting an external weather API to the AI model so it can provide real-time weather information based on user input." + category: Scenarios + languages: + .NET: + versions: + - sdk_version: 3 + github: dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario + excerpts: + - description: "The primary execution of the scenario flow. This scenario orchestrates the conversation between the user, the &BR; Converse API, and a weather tool." + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.Scenario + - description: "The weather tool used by the demo. This file defines the tool specification and implements the logic to retrieve weather data using from the Open-Meteo API." + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.WeatherTool + - description: "The Converse API action with a tool configuration." + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.SendConverseRequest + Python: + versions: + - sdk_version: 3 + github: python/example_code/bedrock-runtime + excerpts: + - description: "The primary execution script of the demo. This script orchestrates the conversation between the user, the &BR; Converse API, and a weather tool." + snippet_files: + - python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo/tool_use_demo.py + - description: "The weather tool used by the demo. This script defines the tool specification and implements the logic to retrieve weather data using from the Open-Meteo API." + snippet_files: + - python/example_code/bedrock-runtime/cross-model-scenarios/tool_use_demo/weather_tool.py + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/bedrock-runtime + excerpts: + - description: "The primary scenario and logic for the demo. This orchestrates the conversation between the user, the &BR; Converse API, and a weather tool." + snippet_tags: + - rust.bedrock-runtime.Converse_AnthropicClaude.tool-use + - description: "The weather tool used by the demo. This script defines the tool specification and implements the logic to retrieve weather data using from the Open-Meteo API." + snippet_tags: + - rust.bedrock-runtime.Converse_AnthropicClaude.tool-use.weather-tool + - description: "Utilities to print the Message Content Blocks." + snippet_tags: + - rust.bedrock-runtime.Converse_AnthropicClaude.tool-use.user-interface + - description: "Use statements, Error utility, and constants." + snippet_tags: + - rust.bedrock-runtime.Converse_AnthropicClaude.tool-use.supporting + services: + bedrock-runtime: {Converse} + bedrock-runtime_Converse_AnthropicClaude: title: Invoke Anthropic Claude on &BR; using Bedrock's Converse API title_abbrev: "Converse" @@ -1315,6 +1373,32 @@ bedrock-runtime_InvokeModelWithResponseStream_TitanTextEmbeddings: bedrock-runtime: {InvokeModel} # Tool use scenarios +bedrock-runtime_Scenario_ToolUseDemo_AmazonNova: + title: "A tool use demo illustrating how to connect AI models on &BR; with a custom tool or API" + title_abbrev: "Scenario: Tool use with the Converse API" + synopsis: "build a typical interaction between an application, a generative AI model, and connected tools or APIs to mediate interactions between the AI and the outside world. It uses the example of connecting an external weather API to the AI model so it can provide real-time weather information based on user input." + category: Amazon Nova + languages: + .NET: + versions: + - sdk_version: 3 + github: dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario + excerpts: + - description: "The primary execution of the scenario flow. This scenario orchestrates the conversation between the user, the &BR; Converse API, and a weather tool." + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.Scenario + - description: "The weather tool used by the demo. This file defines the tool specification and implements the logic to retrieve weather data using from the Open-Meteo API." + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.WeatherTool + - description: "The Converse API action with a tool configuration." + genai: some + snippet_tags: + - Bedrock.ConverseTool.dotnetv3.SendConverseRequest + services: + bedrock-runtime: {Converse} + bedrock-runtime_Scenario_ToolUseDemo_AnthropicClaude: title: "A tool use demo illustrating how to connect AI models on &BR; with a custom tool or API" title_abbrev: "Scenario: Tool use with the Converse API" @@ -1349,7 +1433,6 @@ bedrock-runtime_Scenario_ToolUseDemo_AnthropicClaude: - description: "Use statements, Error utility, and constants." snippet_tags: - rust.bedrock-runtime.Converse_AnthropicClaude.tool-use.supporting - services: bedrock-runtime: {Converse} diff --git a/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln b/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln index d9e5d12e854..dd290cac66d 100644 --- a/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln +++ b/dotnetv3/Bedrock-runtime/BedrockRuntimeExamples.sln @@ -106,6 +106,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNovaCanvas", "AmazonN EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvokeModel", "Models\AmazonNova\AmazonNovaCanvas\InvokeModel\InvokeModel.csproj", "{2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{FDC95D1E-41C6-45A5-BF29-F76FCC3DAEF9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BedrockRuntimeActions", "Actions\BedrockRuntimeActions.csproj", "{ABA0C307-C7A1-4BBE-A7E2-4BA7163559FC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{045D214B-6181-43B0-ABFE-246675F4D967}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConverseToolScenario", "Scenarios\ConverseToolScenario\ConverseToolScenario.csproj", "{C0A5B872-03F5-4865-9349-7A403591C50E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -224,6 +232,14 @@ Global {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D}.Release|Any CPU.Build.0 = Release|Any CPU + {ABA0C307-C7A1-4BBE-A7E2-4BA7163559FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABA0C307-C7A1-4BBE-A7E2-4BA7163559FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABA0C307-C7A1-4BBE-A7E2-4BA7163559FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABA0C307-C7A1-4BBE-A7E2-4BA7163559FC}.Release|Any CPU.Build.0 = Release|Any CPU + {C0A5B872-03F5-4865-9349-7A403591C50E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0A5B872-03F5-4865-9349-7A403591C50E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0A5B872-03F5-4865-9349-7A403591C50E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0A5B872-03F5-4865-9349-7A403591C50E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -266,6 +282,8 @@ Global {E144492A-337A-0755-EAB4-DA083C3A2DDB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {4D3E429C-CCAE-42DE-A062-4717E71D8403} = {3AF63EC9-2EB0-4A0B-8C3B-0CA3595080F6} {2B39D4E2-C6B6-4340-A9AD-5F5C25CA8C1D} = {4D3E429C-CCAE-42DE-A062-4717E71D8403} + {ABA0C307-C7A1-4BBE-A7E2-4BA7163559FC} = {FDC95D1E-41C6-45A5-BF29-F76FCC3DAEF9} + {C0A5B872-03F5-4865-9349-7A403591C50E} = {045D214B-6181-43B0-ABFE-246675F4D967} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E48A5088-1BBB-4A8B-9AB2-CC5CE0482466} diff --git a/dotnetv3/Bedrock-runtime/README.md b/dotnetv3/Bedrock-runtime/README.md index a51ea913fdc..174ab4345b8 100644 --- a/dotnetv3/Bedrock-runtime/README.md +++ b/dotnetv3/Bedrock-runtime/README.md @@ -28,6 +28,13 @@ For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv3 +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Tool use with the Converse API](Scenarios/ConverseToolScenario/ConverseToolScenario.cs) + ### AI21 Labs Jurassic-2 - [Converse](Models/Ai21LabsJurassic2/Converse/Converse.cs#L4) @@ -37,6 +44,7 @@ For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv3 - [Converse](Models/AmazonNova/AmazonNovaText/Converse/Converse.cs#L4) - [ConverseStream](Models/AmazonNova/AmazonNovaText/ConverseStream/ConverseStream.cs#L4) +- [Scenario: Tool use with the Converse API](Scenarios/ConverseToolScenario/ConverseToolScenario.cs#L4) ### Amazon Nova Canvas @@ -110,6 +118,18 @@ Alternatively, you can run the example from within your IDE. +#### Tool use with the Converse API + +This example shows you how to build a typical interaction between an application, a generative AI model, and connected tools or APIs to mediate interactions between the AI and the outside world. It uses the example of connecting an external weather API to the AI model so it can provide real-time weather information based on user input. + + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/BedrockActionsWrapper.cs b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/BedrockActionsWrapper.cs new file mode 100644 index 00000000000..af2d1859e16 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/BedrockActionsWrapper.cs @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; +using Microsoft.Extensions.Logging; + +namespace ConverseToolScenario; + +// snippet-start:[Bedrock.ConverseTool.dotnetv3.SendConverseRequest] + +///

+/// Wrapper class for interacting with the Amazon Bedrock Converse API. +/// +public class BedrockActionsWrapper +{ + private readonly IAmazonBedrockRuntime _bedrockClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Bedrock Converse API client. + /// The logger instance. + public BedrockActionsWrapper(IAmazonBedrockRuntime bedrockClient, ILogger logger) + { + _bedrockClient = bedrockClient; + _logger = logger; + } + + /// + /// Sends a Converse request to the Amazon Bedrock Converse API. + /// + /// The Bedrock Model Id. + /// A system prompt instruction. + /// The array of messages in the conversation. + /// The specification for a tool. + /// The response of the model. + public async Task SendConverseRequestAsync(string modelId, string systemPrompt, List conversation, ToolSpecification toolSpec) + { + try + { + var request = new ConverseRequest() + { + ModelId = modelId, + System = new List() + { + new SystemContentBlock() + { + Text = systemPrompt + } + }, + Messages = conversation, + ToolConfig = new ToolConfiguration() + { + Tools = new List() + { + new Tool() + { + ToolSpec = toolSpec + } + } + } + }; + + var response = await _bedrockClient.ConverseAsync(request); + + return response; + } + catch (ModelNotReadyException ex) + { + _logger.LogError(ex, "Model not ready, please wait and try again."); + throw; + } + catch (AmazonBedrockRuntimeException ex) + { + _logger.LogError(ex, "Error occurred while sending Converse request."); + throw; + } + } +} +// snippet-end:[Bedrock.ConverseTool.dotnetv3.SendConverseRequest] \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.cs b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.cs new file mode 100644 index 00000000000..f220fd4c3d6 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.cs @@ -0,0 +1,361 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Bedrock.ConverseTool.dotnetv3.Scenario] + +using Amazon; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; +using Amazon.Runtime.Documents; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +namespace ConverseToolScenario; + +public static class ConverseToolScenario +{ + /* + Before running this .NET code example, set up your development environment, including your credentials. + + This demo illustrates a tool use scenario using Amazon Bedrock's Converse API and a weather tool. + The script interacts with a foundation model on Amazon Bedrock to provide weather information based on user + input. It uses the Open-Meteo API (https://open-meteo.com) to retrieve current weather data for a given location. + */ + + public static BedrockActionsWrapper _bedrockActionsWrapper = null!; + public static WeatherTool _weatherTool = null!; + public static bool _interactive = true; + + // Change this string to use a different model with Converse API. + private static string model_id = "amazon.nova-lite-v1:0"; + + private static string system_prompt = @" + You are a weather assistant that provides current weather data for user-specified locations using only + the Weather_Tool, which expects latitude and longitude. Infer the coordinates from the location yourself. + If the user provides coordinates, infer the approximate location and refer to it in your response. + To use the tool, you strictly apply the provided tool specification. + + - Explain your step-by-step process, and give brief updates before each step. + - Only use the Weather_Tool for data. Never guess or make up information. + - Repeat the tool use for subsequent requests if necessary. + - If the tool errors, apologize, explain weather is unavailable, and suggest other options. + - Report temperatures in °C (°F) and wind in km/h (mph). Keep weather reports concise. Sparingly use + emojis where appropriate. + - Only respond to weather queries. Remind off-topic users of your purpose. + - Never claim to search online, access external data, or use tools besides Weather_Tool. + - Complete the entire process until you have all required data before sending the complete response. + " + ; + + private static string default_prompt = "What is the weather like in Seattle?"; + + // The maximum number of recursive calls allowed in the tool use function. + // This helps prevent infinite loops and potential performance issues. + private static int max_recursions = 5; + + public static async Task Main(string[] args) + { + // Set up dependency injection for the Amazon service. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + logging.AddFilter("System", LogLevel.Error) + .AddFilter("Microsoft", LogLevel.Trace)) + .ConfigureServices((_, services) => + services.AddHttpClient() + .AddSingleton(_ => new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1)) // Specify a region that has access to the chosen model. + .AddTransient() + .AddTransient() + .RemoveAll() + ) + .Build(); + + ServicesSetup(host); + + try + { + await RunConversationAsync(); + + } + catch (Exception ex) + { + Console.WriteLine(new string('-', 80)); + Console.WriteLine($"There was a problem running the scenario: {ex.Message}"); + Console.WriteLine(new string('-', 80)); + } + finally + { + Console.WriteLine( + "Amazon Bedrock Converse API with Tool Use Feature Scenario is complete."); + Console.WriteLine(new string('-', 80)); + } + } + + /// + /// Populate the services for use within the console application. + /// + /// The services host. + private static void ServicesSetup(IHost host) + { + _bedrockActionsWrapper = host.Services.GetRequiredService(); + _weatherTool = host.Services.GetRequiredService(); + } + + /// + /// Starts the conversation with the user and handles the interaction with Bedrock. + /// + /// The conversation array. + public static async Task> RunConversationAsync() + { + // Print the greeting and a short user guide + PrintHeader(); + + // Start with an empty conversation + var conversation = new List(); + + // Get the first user input + var userInput = await GetUserInputAsync(); + + while (userInput != null) + { + // Create a new message with the user input and append it to the conversation + var message = new Message { Role = ConversationRole.User, Content = new List { new ContentBlock { Text = userInput } } }; + conversation.Add(message); + + // Send the conversation to Amazon Bedrock + var bedrockResponse = await SendConversationToBedrock(conversation); + + // Recursively handle the model's response until the model has returned its final response or the recursion counter has reached 0 + await ProcessModelResponseAsync(bedrockResponse, conversation, max_recursions); + + // Repeat the loop until the user decides to exit the application + userInput = await GetUserInputAsync(); + } + + PrintFooter(); + return conversation; + } + + /// + /// Sends the conversation, the system prompt, and the tool spec to Amazon Bedrock, and returns the response. + /// + /// The conversation history including the next message to send. + /// The response from Amazon Bedrock. + private static async Task SendConversationToBedrock(List conversation) + { + Console.WriteLine("\tCalling Bedrock..."); + + // Send the conversation, system prompt, and tool configuration, and return the response + return await _bedrockActionsWrapper.SendConverseRequestAsync(model_id, system_prompt, conversation, _weatherTool.GetToolSpec()); + } + + /// + /// Processes the response received via Amazon Bedrock and performs the necessary actions based on the stop reason. + /// + /// The model's response returned via Amazon Bedrock. + /// The conversation history. + /// The maximum number of recursive calls allowed. + private static async Task ProcessModelResponseAsync(ConverseResponse modelResponse, List conversation, int maxRecursion) + { + if (maxRecursion <= 0) + { + // Stop the process, the number of recursive calls could indicate an infinite loop + Console.WriteLine("\tWarning: Maximum number of recursions reached. Please try again."); + } + + // Append the model's response to the ongoing conversation + conversation.Add(modelResponse.Output.Message); + + if (modelResponse.StopReason == "tool_use") + { + // If the stop reason is "tool_use", forward everything to the tool use handler + await HandleToolUseAsync(modelResponse.Output, conversation, maxRecursion - 1); + } + + if (modelResponse.StopReason == "end_turn") + { + // If the stop reason is "end_turn", print the model's response text, and finish the process + PrintModelResponse(modelResponse.Output.Message.Content[0].Text); + if (!_interactive) + { + default_prompt = "x"; + } + } + } + + /// + /// Handles the tool use case by invoking the specified tool and sending the tool's response back to Bedrock. + /// The tool response is appended to the conversation, and the conversation is sent back to Amazon Bedrock for further processing. + /// + /// The model's response containing the tool use request. + /// The conversation history. + /// The maximum number of recursive calls allowed. + public static async Task HandleToolUseAsync(ConverseOutput modelResponse, List conversation, int maxRecursion) + { + // Initialize an empty list of tool results + var toolResults = new List(); + + // The model's response can consist of multiple content blocks + foreach (var contentBlock in modelResponse.Message.Content) + { + if (!String.IsNullOrEmpty(contentBlock.Text)) + { + // If the content block contains text, print it to the console + PrintModelResponse(contentBlock.Text); + } + + if (contentBlock.ToolUse != null) + { + // If the content block is a tool use request, forward it to the tool + var toolResponse = await InvokeTool(contentBlock.ToolUse); + + // Add the tool use ID and the tool's response to the list of results + toolResults.Add(new ContentBlock + { + ToolResult = new ToolResultBlock() + { + ToolUseId = toolResponse.ToolUseId, + Content = new List() + { new ToolResultContentBlock { Json = toolResponse.Content } } + } + }); + } + } + + // Embed the tool results in a new user message + var message = new Message() { Role = ConversationRole.User, Content = toolResults }; + + // Append the new message to the ongoing conversation + conversation.Add(message); + + // Send the conversation to Amazon Bedrock + var response = await SendConversationToBedrock(conversation); + + // Recursively handle the model's response until the model has returned its final response or the recursion counter has reached 0 + await ProcessModelResponseAsync(response, conversation, maxRecursion); + } + + /// + /// Invokes the specified tool with the given payload and returns the tool's response. + /// If the requested tool does not exist, an error message is returned. + /// + /// The payload containing the tool name and input data. + /// The tool's response or an error message. + public static async Task InvokeTool(ToolUseBlock payload) + { + var toolName = payload.Name; + + if (toolName == "Weather_Tool") + { + var inputData = payload.Input.AsDictionary(); + PrintToolUse(toolName, inputData); + + // Invoke the weather tool with the input data provided + var weatherResponse = await _weatherTool.FetchWeatherDataAsync(inputData["latitude"].ToString(), inputData["longitude"].ToString()); + return new ToolResponse { ToolUseId = payload.ToolUseId, Content = weatherResponse }; + } + else + { + var errorMessage = $"\tThe requested tool with name '{toolName}' does not exist."; + return new ToolResponse { ToolUseId = payload.ToolUseId, Content = new { error = true, message = errorMessage } }; + } + } + + + /// + /// Prompts the user for input and returns the user's response. + /// Returns null if the user enters 'x' to exit. + /// + /// The prompt to display to the user. + /// The user's input or null if the user chooses to exit. + private static async Task GetUserInputAsync(string prompt = "\tYour weather info request:") + { + var userInput = default_prompt; + if (_interactive) + { + Console.WriteLine(new string('*', 80)); + Console.WriteLine($"{prompt} (x to exit): \n\t"); + userInput = Console.ReadLine(); + } + + if (string.IsNullOrWhiteSpace(userInput)) + { + prompt = "\tPlease enter your weather info request, e.g. the name of a city"; + return await GetUserInputAsync(prompt); + } + + if (userInput.ToLowerInvariant() == "x") + { + return null; + } + + return userInput; + } + + /// + /// Logs the welcome message and usage guide for the tool use demo. + /// + public static void PrintHeader() + { + Console.WriteLine(@" + ================================================= + Welcome to the Amazon Bedrock Tool Use demo! + ================================================= + + This assistant provides current weather information for user-specified locations. + You can ask for weather details by providing the location name or coordinates. Weather information + will be provided using a custom Tool and open-meteo API. + + Example queries: + - What's the weather like in New York? + - Current weather for latitude 40.70, longitude -74.01 + - Is it warmer in Rome or Barcelona today? + + To exit the program, simply type 'x' and press Enter. + + P.S.: You're not limited to single locations, or even to using English! + Have fun and experiment with the app! + "); + } + + /// + /// Logs the footer information for the tool use demo. + /// + public static void PrintFooter() + { + Console.WriteLine(@" + ================================================= + Thank you for checking out the Amazon Bedrock Tool Use demo. We hope you + learned something new, or got some inspiration for your own apps today! + + For more Bedrock examples in different programming languages, have a look at: + https://docs.aws.amazon.com/bedrock/latest/userguide/service_code_examples.html + ================================================= + "); + } + + /// + /// Logs information about the tool use. + /// + /// The name of the tool being used. + /// The input data for the tool. + public static void PrintToolUse(string toolName, Dictionary inputData) + { + Console.WriteLine($"\n\tInvoking tool: {toolName} with input: {inputData["latitude"].ToString()}, {inputData["longitude"].ToString()}...\n"); + } + + /// + /// Logs the model's response. + /// + /// The model's response message. + public static void PrintModelResponse(string message) + { + Console.WriteLine("\tThe model's response:\n"); + Console.WriteLine(message); + Console.WriteLine(); + } +} +// snippet-end:[Bedrock.ConverseTool.dotnetv3.Scenario] \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.csproj b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.csproj new file mode 100644 index 00000000000..6d77e9066e9 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ConverseToolScenario.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/README.md b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/README.md new file mode 100644 index 00000000000..0c052ac1e45 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/README.md @@ -0,0 +1,59 @@ +# Bedrock Runtime Converse API with Tool Feature Scenario + +## Overview + +This example shows how to use AWS SDKs and the Amazon Bedrock Converse API to call a custom tool from a large language model (LLM) as part of a multistep conversation. The example creates a weather tool that leverages the Open-Meteo API to retrieve current weather information based on user input. + +[Bedrock Converse API with Tool Definition](https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use-inference-call.html). + +## ⚠ Important + +* Running this code might result in charges to your AWS account. +* 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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + +## Scenario + +This example illustrates a typical interaction between a generative AI model, an application, and connected tools or APIs to solve a problem or achieve a specific goal. The scenario follows these steps: + +1. Set up the system prompt and tool configuration. +2. Specify the AI model to be used (e.g., Anthropic Claude 3 Sonnet). +3. Create a client to interact with Amazon Bedrock. +4. Prompt the user for their weather request. +5. Send the user input including the conversation history to the model. +6. The model processes the input and determines if a connected tool or API needs to be used. If this is the case, the model returns a tool use request with specific parameters needed to invoke the tool, and a unique tool use ID to correlate tool responses to the request. +7. The scenario application invokes the tool to fetch weather data, and append the response and tool use ID to the conversation. +8. The model uses the tool response to generate a final response. If additional tool requests are needed, the process is repeated. +9. Once the final response is received and printed, the application returns to the prompt. + +### Prerequisites + +For general prerequisites, see the [README](../../../README.md) in the `dotnetv3` folder. + +### Resources + +No additional resources are needed for this scenario. + +### Instructions + +After the example compiles, you can run it from the command line. To do so, navigate to +the folder that contains the .sln file and run the following command: + +``` +dotnet run +``` + +Alternatively, you can run the example from within your IDE. + +This starts an interactive scenario that walks you through exploring conditional requests for read, write, and copy operations. + +## Additional resources + +- [Documentation: The Amazon Bedrock User Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) +- [Tutorials: A developer's guide to Bedrock's new Converse API](https://community.aws/content/2dtauBCeDa703x7fDS9Q30MJoBA/amazon-bedrock-converse-api-developer-guide) +- [More examples: Amazon Bedrock code examples and scenarios in multiple programming languages](https://docs.aws.amazon.com/bedrock/latest/userguide/service_code_examples.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ToolResponse.cs b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ToolResponse.cs new file mode 100644 index 00000000000..95dbb986fa1 --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/ToolResponse.cs @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Bedrock.ConverseTool.dotnetv3.ToolResponse] + +namespace ConverseToolScenario; + +/// +/// Response object for the tool results. +/// +public class ToolResponse +{ + public string ToolUseId { get; set; } = null!; + public dynamic Content { get; set; } = null!; +} +// snippet-end:[Bedrock.ConverseTool.dotnetv3.ToolResponse] \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/WeatherTool.cs b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/WeatherTool.cs new file mode 100644 index 00000000000..1e87b25927b --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/WeatherTool.cs @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[Bedrock.ConverseTool.dotnetv3.WeatherTool] + +using Amazon.BedrockRuntime.Model; +using Amazon.Runtime.Documents; +using Microsoft.Extensions.Logging; + +namespace ConverseToolScenario; + +/// +/// Weather tool that will be invoked when requested by the Bedrock response. +/// +public class WeatherTool +{ + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public WeatherTool(ILogger logger, IHttpClientFactory httpClientFactory) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + /// + /// Returns the JSON Schema specification for the Weather tool. The tool specification + /// defines the input schema and describes the tool's functionality. + /// For more information, see https://json-schema.org/understanding-json-schema/reference. + /// + /// The tool specification for the Weather tool. + public ToolSpecification GetToolSpec() + { + ToolSpecification toolSpecification = new ToolSpecification(); + + toolSpecification.Name = "Weather_Tool"; + toolSpecification.Description = "Get the current weather for a given location, based on its WGS84 coordinates."; + + Document toolSpecDocument = Document.FromObject( + new + { + type = "object", + properties = new + { + latitude = new + { + type = "string", + description = "Geographical WGS84 latitude of the location." + }, + longitude = new + { + type = "string", + description = "Geographical WGS84 longitude of the location." + } + }, + required = new[] { "latitude", "longitude" } + }); + + toolSpecification.InputSchema = new ToolInputSchema() { Json = toolSpecDocument }; + return toolSpecification; + } + + /// + /// Fetches weather data for the given latitude and longitude using the Open-Meteo API. + /// Returns the weather data or an error message if the request fails. + /// + /// The latitude of the location. + /// The longitude of the location. + /// The weather data or an error message. + public async Task FetchWeatherDataAsync(string latitude, string longitude) + { + string endpoint = "https://api.open-meteo.com/v1/forecast"; + + try + { + var httpClient = _httpClientFactory.CreateClient(); + var response = await httpClient.GetAsync($"{endpoint}?latitude={latitude}&longitude={longitude}¤t_weather=True"); + response.EnsureSuccessStatusCode(); + var weatherData = await response.Content.ReadAsStringAsync(); + + Document weatherDocument = Document.FromObject( + new { weather_data = weatherData }); + + return weatherDocument; + } + catch (HttpRequestException e) + { + _logger.LogError(e, "Error fetching weather data: {Message}", e.Message); + throw; + } + catch (Exception e) + { + _logger.LogError(e, "Unexpected error fetching weather data: {Message}", e.Message); + throw; + } + } +} +// snippet-end:[Bedrock.ConverseTool.dotnetv3.WeatherTool] \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj b/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj index ef74e91feaf..6c0e8620b3e 100644 --- a/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj +++ b/dotnetv3/Bedrock-runtime/Tests/BedrockRuntimeTests.csproj @@ -13,9 +13,9 @@ + - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -55,6 +55,7 @@ + \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Tests/ConverseToolScenarioTests.cs b/dotnetv3/Bedrock-runtime/Tests/ConverseToolScenarioTests.cs new file mode 100644 index 00000000000..f5660a3774b --- /dev/null +++ b/dotnetv3/Bedrock-runtime/Tests/ConverseToolScenarioTests.cs @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon; +using Amazon.BedrockRuntime; +using ConverseToolScenario; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace BedrockRuntimeTests; + +/// +/// Tests for the Converse Tool Use example. +/// +public class ConverseToolScenarioTests +{ + private readonly BedrockActionsWrapper _bedrockActionsWrapper = null!; + private readonly WeatherTool _weatherTool = null!; + private readonly ILoggerFactory _loggerFactory; + + /// + /// Constructor for the test class. + /// + public ConverseToolScenarioTests() + { + + _loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + + IServiceCollection services = new ServiceCollection(); // [1] + + services.AddHttpClient(); + + IHttpClientFactory _httpClientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + + _bedrockActionsWrapper = new BedrockActionsWrapper( + new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1), new Logger(_loggerFactory)); + _weatherTool = new WeatherTool(new Logger(_loggerFactory), + _httpClientFactory); + ConverseToolScenario.ConverseToolScenario._bedrockActionsWrapper = _bedrockActionsWrapper; + ConverseToolScenario.ConverseToolScenario._weatherTool = _weatherTool; + } + + /// + /// Run the non-interactive scenario. Should return a non-empty conversation. + /// + /// Async task. + [Fact] + [Trait("Category", "Integration")] + public async Task TestScenario() + { + // Arrange. + ConverseToolScenario.ConverseToolScenario._interactive = false; + + // Act. + var conversation = await ConverseToolScenario.ConverseToolScenario.RunConversationAsync(); + + // Assert. + Assert.NotEmpty(conversation); + } +} \ No newline at end of file diff --git a/dotnetv3/Bedrock-runtime/Tests/GlobalUsings.cs b/dotnetv3/Bedrock-runtime/Tests/GlobalUsings.cs index ef5ce323ba9..0f64a5599c7 100644 --- a/dotnetv3/Bedrock-runtime/Tests/GlobalUsings.cs +++ b/dotnetv3/Bedrock-runtime/Tests/GlobalUsings.cs @@ -2,11 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 global using Xunit; -global using Xunit.Extensions.Ordering; // Optional. -[assembly: CollectionBehavior(DisableTestParallelization = true)] -// Optional. -[assembly: TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")] -// Optional. -[assembly: TestCollectionOrderer("Xunit.Extensions.Ordering.CollectionOrderer", "Xunit.Extensions.Ordering")] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/dotnetv3/DotNetV3Examples.sln b/dotnetv3/DotNetV3Examples.sln index 54a02263eb2..c6f351d608b 100644 --- a/dotnetv3/DotNetV3Examples.sln +++ b/dotnetv3/DotNetV3Examples.sln @@ -837,6 +837,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S3ObjectLockScenario", "S3\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S3ObjectLockTests", "S3\scenarios\S3ObjectLockScenario\S3ObjectLockTests\S3ObjectLockTests.csproj", "{BCCFBED0-E800-46C5-975B-7D404486F00F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConverseToolScenario", "Bedrock-runtime\Scenarios\ConverseToolScenario\ConverseToolScenario.csproj", "{83ED7BBE-5C9A-47AC-805B-351270069570}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1915,6 +1917,10 @@ Global {BCCFBED0-E800-46C5-975B-7D404486F00F}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCCFBED0-E800-46C5-975B-7D404486F00F}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCCFBED0-E800-46C5-975B-7D404486F00F}.Release|Any CPU.Build.0 = Release|Any CPU + {83ED7BBE-5C9A-47AC-805B-351270069570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83ED7BBE-5C9A-47AC-805B-351270069570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83ED7BBE-5C9A-47AC-805B-351270069570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83ED7BBE-5C9A-47AC-805B-351270069570}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2296,6 +2302,7 @@ Global {7EC94891-9A5F-47EF-9C97-8A280754525C} = {0169CEB9-B6A7-447D-921D-C79358DDCCE6} {93588ED1-A248-4F6C-85A4-27E9E65D8AC7} = {7EC94891-9A5F-47EF-9C97-8A280754525C} {BCCFBED0-E800-46C5-975B-7D404486F00F} = {7EC94891-9A5F-47EF-9C97-8A280754525C} + {83ED7BBE-5C9A-47AC-805B-351270069570} = {BA23BB28-EC63-4330-8CA7-DEB1B6489580} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} diff --git a/dotnetv4/Bedrock/Actions/BedrockActions.csproj b/dotnetv4/Bedrock/Actions/BedrockActions.csproj index 9f12aa3e3f2..ffee5ec19c7 100644 --- a/dotnetv4/Bedrock/Actions/BedrockActions.csproj +++ b/dotnetv4/Bedrock/Actions/BedrockActions.csproj @@ -9,6 +9,7 @@ + diff --git a/python/example_code/bedrock-runtime/README.md b/python/example_code/bedrock-runtime/README.md index 91bca8633dc..68a71a78623 100644 --- a/python/example_code/bedrock-runtime/README.md +++ b/python/example_code/bedrock-runtime/README.md @@ -43,6 +43,13 @@ python -m pip install -r requirements.txt - [Hello Amazon Bedrock Runtime](hello/hello_bedrock_runtime_invoke.py#L5) (`InvokeModel`) +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Tool use with the Converse API](cross-model-scenarios/tool_use_demo/tool_use_demo.py) + ### AI21 Labs Jurassic-2 - [Converse](models/ai21_labs_jurassic2/converse.py#L4) @@ -143,6 +150,24 @@ python hello/hello_bedrock_runtime_invoke.py ``` +#### Tool use with the Converse API + +This example shows you how to build a typical interaction between an application, a generative AI model, and connected tools or APIs to mediate interactions between the AI and the outside world. It uses the example of connecting an external weather API to the AI model so it can provide real-time weather information based on user input. + + + + + +Start the example by running the following at a command prompt: + +``` +python cross-model-scenarios/tool_use_demo/tool_use_demo.py +``` + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/rustv1/examples/bedrock-runtime/README.md b/rustv1/examples/bedrock-runtime/README.md index ae64bcc7542..abb0df99ce4 100644 --- a/rustv1/examples/bedrock-runtime/README.md +++ b/rustv1/examples/bedrock-runtime/README.md @@ -28,6 +28,13 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Tool use with the Converse API](src/bin/tool-use.rs) + ### Anthropic Claude - [Converse](src/bin/converse.rs#L43) @@ -48,6 +55,18 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv +#### Tool use with the Converse API + +This example shows you how to build a typical interaction between an application, a generative AI model, and connected tools or APIs to mediate interactions between the AI and the outside world. It uses the example of connecting an external weather API to the AI model so it can provide real-time weather information based on user input. + + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. @@ -74,4 +93,4 @@ in the `rustv1` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/scenarios/features/bedrock_converse_tool/README.md b/scenarios/features/bedrock_converse_tool/README.md new file mode 100644 index 00000000000..3e8cb087dcf --- /dev/null +++ b/scenarios/features/bedrock_converse_tool/README.md @@ -0,0 +1,42 @@ +# Bedrock Runtime Converse API with Tool Feature Scenario + +## Overview + +This example shows how to use AWS SDKs and the Amazon Bedrock Converse API to call a custom tool from a large language model (LLM) as part of a multistep conversation. The example creates a weather tool that leverages the Open-Meteo API to retrieve current weather information based on user input. + +[Bedrock Converse API with Tool Definition](https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use-inference-call.html). + +This example illustrates a typical interaction between a generative AI model, an application, and connected tools or APIs to solve a problem or achieve a specific goal. The scenario follows these steps: + +1. Set up the system prompt and tool configuration. +2. Create a client to interact with Amazon Bedrock. +3. Prompt the user for their weather request. +4. Send the user input including the conversation history to the model. +5. The model processes the input and determines if a connected tool or API needs to be used. If this is the case, the model returns a tool use request with specific parameters needed to invoke the tool, and a unique tool use ID to correlate tool responses to the request. +6. The scenario application invokes the tool to fetch weather data, and append the response and tool use ID to the conversation. +7. The model uses the tool response to generate a final response. If additional tool requests are needed, the process is repeated. If the max recursion is reached, the conversation is ended. +8. Once the final response is received and printed, the application returns to the prompt. + +![img.png](toolscenario.png) + +![img.png](toolscenario.png) + +### Resources + +No additional resources are needed for this scenario. + +## Implementations + +This example is implemented in the following languages: + +- [.NET](../../../dotnetv3/Bedrock-runtime/Scenarios/ConverseToolScenario/README.md) + +## Additional resources + +- [Documentation: The Amazon Bedrock User Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) +- [Tutorials: A developer's guide to Bedrock's new Converse API](https://community.aws/content/2dtauBCeDa703x7fDS9Q30MJoBA/amazon-bedrock-converse-api-developer-guide) +- [More examples: Amazon Bedrock code examples and scenarios in multiple programming languages](https://docs.aws.amazon.com/bedrock/latest/userguide/service_code_examples.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/scenarios/features/bedrock_converse_tool/SPECIFICATION.md b/scenarios/features/bedrock_converse_tool/SPECIFICATION.md new file mode 100644 index 00000000000..474980999a9 --- /dev/null +++ b/scenarios/features/bedrock_converse_tool/SPECIFICATION.md @@ -0,0 +1,246 @@ +# Bedrock Runtime Converse API with Tool Feature Scenario - Technical specification + +This document contains the technical specifications for _Bedrock Runtime Converse API with Tool Feature Scenario_, a feature scenario that showcases AWS services and SDKs. It is primarily intended for the AWS code examples team to use while developing this example in additional languages. + +This document explains the following: + +- Architecture and features of the example scenario. +- Metadata information for the scenario. +- Sample reference output. + +For an introduction, see the [README.md](README.md). + +--- + +### Table of contents + +- [User Input](#user-input) +- [Example Output](#example-output) +- [Errors](#errors) +- [Metadata](#metadata) + +## User Input + +The user's input is used as the starting point for the Bedrock Runtime conversation, and each response is added to an array of messages. +The model should respond when it needs to invoke the tool, and the application should run the tool and append the response to the conversation. +This process can be repeated as needed until a maximum number of recursions (5). See the .NET implementation for an example of the processing of the messages. Following is an example of how the conversation could go: + +1. Greet the user and provide an overview of the application. +1. Handle the user's weather information request: + 1. The user requests weather information. This request is sent to the Bedrock model. + 2. The model response includes a tool request, with a latitude and longitude to provide to the tool. + 3. The application then uses the Weather_Tool to retrieve the current weather data for those coordinates, and appends that response as a tool response to the conversation. The conversation is sent back to the model. + 4. The model responds with either a final response, or a request for more information. The process repeats. + 5. The application prints the final response. +1. Any off topic requests should be handled according to the system prompt. This prompt is provided below. +1. The user can type 'x' to exit the application. + +#### System prompt +``` +You are a weather assistant that provides current weather data for user-specified locations using only +the Weather_Tool, which expects latitude and longitude. Infer the coordinates from the location yourself. +If the user provides coordinates, infer the approximate location and refer to it in your response. +To use the tool, you strictly apply the provided tool specification. + +- Explain your step-by-step process, and give brief updates before each step. +- Only use the Weather_Tool for data. Never guess or make up information. +- Repeat the tool use for subsequent requests if necessary. +- If the tool errors, apologize, explain weather is unavailable, and suggest other options. +- Report temperatures in °C (°F) and wind in km/h (mph). Keep weather reports concise. Sparingly use + emojis where appropriate. +- Only respond to weather queries. Remind off-topic users of your purpose. +- Never claim to search online, access external data, or use tools besides Weather_Tool. +- Complete the entire process until you have all required data before sending the complete response. +``` + +#### Weather tool specification +For strongly typed languages, you will need to use the Bedrock classes provided for tool specification. + +``` +"toolSpec": { + "name": "Weather_Tool", + "description": "Get the current weather for a given location, based on its WGS84 coordinates.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "latitude": { + "type": "string", + "description": "Geographical WGS84 latitude of the location.", + }, + "longitude": { + "type": "string", + "description": "Geographical WGS84 longitude of the location.", + }, + }, + "required": ["latitude", "longitude"], + } + }, + } +``` + + +## Example Output +``` +******************************************************************************** + Welcome to the Amazon Bedrock Tool Use demo! +******************************************************************************** + + This assistant provides current weather information for user-specified locations. + You can ask for weather details by providing the location name or coordinates. + + Example queries: + - What's the weather like in New York? + - Current weather for latitude 40.70, longitude -74.01 + - Is it warmer in Rome or Barcelona today? + + To exit the program, simply type 'x' and press Enter. + + P.S.: You're not limited to single locations, or even to using English! + Have fun and experiment with the app! + +******************************************************************************** + Your weather info request: (x to exit): + +>What's the weather like in Oklahoma City? + Calling Bedrock... + The model's response: + +Okay, let me get the current weather information for Oklahoma City: + +1) I will look up the latitude and longitude coordinates for Oklahoma City. +2) Then I will use the Weather_Tool to get the weather data for those coordinates. + + + Invoking tool: Weather_Tool with input: 35.4676, -97.5164... + + Calling Bedrock... + The model's response: + +According to the weather data, the current conditions in Oklahoma City are: + +??? Partly cloudy +Temperature: 2.7°C (36.9°F) +Wind: 22.3 km/h (13.9 mph) from the North + +The wind is breezy and it's a bit cool for this time of year in Oklahoma City. I'd recommend wearing a jacket if going outside for extended periods. + +******************************************************************************** + Your weather info request: (x to exit): + +>What's the best kind of cat? + Calling Bedrock... + The model's response: + +I'm an AI assistant focused on providing current weather information using the available Weather_Tool. I don't have any data or capabilities related to discussing different types of cats. Perhaps we could return to discussing weather conditions for a particular location? I'd be happy to look up the latest forecast if you provide a city or geographic coordinates. + +******************************************************************************** + Your weather info request: (x to exit): + +>Where is the warmest city in Oklahoma right now? + Calling Bedrock... + The model's response: + +Okay, let me see if I can find the warmest city in Oklahoma right now using the Weather_Tool: + +1) I will look up the coordinates for some major cities in Oklahoma. +2) Then I will use the Weather_Tool to get the current temperature for each city. +3) I will compare the temperatures to determine the warmest city. + + + Invoking tool: Weather_Tool with input: 35.4676, -97.5164... + + Calling Bedrock... + The model's response: + +Oklahoma City: 2.7°C + + + Invoking tool: Weather_Tool with input: 36.1539, -95.9925... + + Calling Bedrock... + The model's response: + +Tulsa: 5.5°C + +Based on the data from the Weather_Tool, the warmest major city in Oklahoma right now is Tulsa at 5.5°C (41.9°F). + +******************************************************************************** + Your weather info request: (x to exit): + +>What's the warmest city in California right now? + Calling Bedrock... + The model's response: + +OK, let me check the current temperatures in some major cities in California to find the warmest one: + + + Invoking tool: Weather_Tool with input: 34.0522, -118.2437... + + Calling Bedrock... + The model's response: + +Los Angeles: 10.6°C (51.1°F) + + + Invoking tool: Weather_Tool with input: 37.7749, -122.4194... + + Calling Bedrock... + The model's response: + + + +San Francisco: 11.6°C (52.9°F) + + + Invoking tool: Weather_Tool with input: 32.7157, -117.1611... + + Calling Bedrock... + Warning: Maximum number of recursions reached. Please try again. + The model's response: + +San Diego: 12.9°C (55.2°F) + +Based on the data from the Weather_Tool, the warmest major city in California right now appears to be San Diego at 12.9°C (55.2°F). + +******************************************************************************** + Your weather info request: (x to exit): +>x +******************************************************************************** + Thank you for checking out the Amazon Bedrock Tool Use demo. We hope you + learned something new, or got some inspiration for your own apps today! + + For more Bedrock examples in different programming languages, have a look at: + https://docs.aws.amazon.com/bedrock/latest/userguide/service_code_examples.html +******************************************************************************** + +Amazon Bedrock Converse API with Tool Use Feature Scenario is complete. +-------------------------------------------------------------------------------- + +``` +- Cleanup + - There are no resources needing cleanup in this scenario. + +--- + +## Errors +In addition to handling Bedrock Runtime errors on the Converse action, the scenario should also +handle errors related to the tool itself, such as an HTTP Request failure. + +| action | Error | Handling | +|----------------|------------------------|------------------------------------------------------| +| `Converse` | ModelNotReady | Notify the user to try again, and stop the scenario. | +| `HTTP Request` | HttpRequestException | Notify the user and stop the scenario. | + +--- + +## Metadata +For languages which already have an entry for the action, add a description for the snippet describing the scenario or action. + +| action / scenario | metadata file | metadata key | +|--------------------------------------------|--------------------------------|------------------------------------------------------| +| `Converse` | bedrock-runtime_metadata.yaml | bedrock-runtime_Converse_AmazonNovaText | +| `Tool use with the Converse API` | bedrock-runtime_metadata.yaml | bedrock-runtime_Scenario_ToolUse | +| `Scenario: Tool use with the Converse API` | bedrock-runtime_metadata.yaml | bedrock-runtime_Scenario_ToolUseDemo_AmazonNova | + + diff --git a/scenarios/features/bedrock_converse_tool/toolscenario.png b/scenarios/features/bedrock_converse_tool/toolscenario.png new file mode 100644 index 0000000000000000000000000000000000000000..45defe47143627a25da8d415306b045a6ff17359 GIT binary patch literal 40313 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 literal 0 HcmV?d00001 From 60b2bbfea3c7f12b6d3babbaf1e866782c3b76cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:17:11 +0000 Subject: [PATCH 064/144] Bump vite from 4.5.5 to 4.5.9 in /applications/feedback_sentiment_analyzer/client (#7255) Bump vite in /applications/feedback_sentiment_analyzer/client Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.5 to 4.5.9. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v4.5.9/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v4.5.9/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../client/package-lock.json | 15 ++++++++------- .../client/package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/applications/feedback_sentiment_analyzer/client/package-lock.json b/applications/feedback_sentiment_analyzer/client/package-lock.json index 69247a2bd80..6db1c4f50c8 100644 --- a/applications/feedback_sentiment_analyzer/client/package-lock.json +++ b/applications/feedback_sentiment_analyzer/client/package-lock.json @@ -20,7 +20,7 @@ "@types/react-dom": "^18.2.4", "@vitejs/plugin-react": "^4.0.1", "typescript": "^5.1.3", - "vite": "^4.5.5" + "vite": "^4.5.9" } }, "node_modules/@ampproject/remapping": { @@ -1685,10 +1685,11 @@ } }, "node_modules/vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", + "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -2894,9 +2895,9 @@ "requires": {} }, "vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", + "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, "requires": { "esbuild": "^0.18.10", diff --git a/applications/feedback_sentiment_analyzer/client/package.json b/applications/feedback_sentiment_analyzer/client/package.json index 52e08a08e67..332c5a9ce7c 100644 --- a/applications/feedback_sentiment_analyzer/client/package.json +++ b/applications/feedback_sentiment_analyzer/client/package.json @@ -21,6 +21,6 @@ "@types/react-dom": "^18.2.4", "@vitejs/plugin-react": "^4.0.1", "typescript": "^5.1.3", - "vite": "^4.5.5" + "vite": "^4.5.9" } } From 35f7d245c022f63fd7e660d4d0adb77f3c96bd73 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 24 Feb 2025 15:27:24 -0500 Subject: [PATCH 065/144] Applied review changes --- javav2/example_code/entityresolution/pom.xml | 8 + .../entity/scenario/CloudFormationHelper.java | 188 ++++++++ .../entity/scenario/EntityResActions.java | 423 ++++++++++-------- .../entity/scenario/EntityResScenario.java | 196 +++++--- .../src/main/resources/TODO.md | 8 + .../src/main/resources/data.csv | 5 + .../src/main/resources/data.json | 3 + .../resources/{glue.yaml => template.yaml} | 107 +++-- .../src/test/java/EntityResTests.java | 3 +- .../cdk/entityresolution_resources/README.md | 48 +- .../cdk/entityresolution_resources/cdk.json | 68 +++ .../com/myorg/EntityResolutionCdkStack.java | 126 ++++-- scenarios/basics/entity_resolution/README.md | 22 +- .../basics/entity_resolution/SPECIFICATION.md | 123 +++-- 14 files changed, 908 insertions(+), 420 deletions(-) create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java create mode 100644 javav2/example_code/entityresolution/src/main/resources/TODO.md create mode 100644 javav2/example_code/entityresolution/src/main/resources/data.csv create mode 100644 javav2/example_code/entityresolution/src/main/resources/data.json rename javav2/example_code/entityresolution/src/main/resources/{glue.yaml => template.yaml} (72%) create mode 100644 resources/cdk/entityresolution_resources/cdk.json diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index d2fee1f06c4..19684620c48 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -92,6 +92,14 @@ software.amazon.awssdk cloudformation + + software.amazon.awssdk + sso + + + software.amazon.awssdk + ssooidc + org.apache.logging.log4j log4j-core diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java new file mode 100644 index 00000000000..9de189ea437 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java @@ -0,0 +1,188 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity.scenario; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient; +import software.amazon.awssdk.services.cloudformation.model.Capability; +import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse; +import software.amazon.awssdk.services.cloudformation.model.Output; +import software.amazon.awssdk.services.cloudformation.model.Stack; +import software.amazon.awssdk.services.cloudformation.waiters.CloudFormationAsyncWaiter; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class CloudFormationHelper { + private static final String CFN_TEMPLATE = "template.yaml"; + private static final Logger logger = LoggerFactory.getLogger(CloudFormationHelper.class); + + private static CloudFormationAsyncClient cloudFormationClient; + + public static void main(String[] args) { + emptyS3Bucket(args[0]); + } + + private static CloudFormationAsyncClient getCloudFormationClient() { + if (cloudFormationClient == null) { + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .connectionTimeout(Duration.ofSeconds(60)) + .readTimeout(Duration.ofSeconds(60)) + .writeTimeout(Duration.ofSeconds(60)) + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) + .apiCallAttemptTimeout(Duration.ofSeconds(90)) + .retryStrategy(RetryMode.STANDARD) + .build(); + + cloudFormationClient = CloudFormationAsyncClient.builder() + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return cloudFormationClient; + } + + public static void deployCloudFormationStack(String stackName) { + String templateBody; + boolean doesExist = describeStack(stackName); + if (!doesExist) { + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Path filePath = Paths.get(classLoader.getResource(CFN_TEMPLATE).toURI()); + templateBody = Files.readString(filePath); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + + getCloudFormationClient().createStack(b -> b.stackName(stackName) + .templateBody(templateBody) + .capabilities(Capability.CAPABILITY_IAM)) + .whenComplete((csr, t) -> { + if (csr != null) { + System.out.println("Stack creation requested, ARN is " + csr.stackId()); + try (CloudFormationAsyncWaiter waiter = getCloudFormationClient().waiter()) { + waiter.waitUntilStackCreateComplete(request -> request.stackName(stackName)) + .whenComplete((dsr, th) -> { + if (th != null) { + System.out.println("Error waiting for stack creation: " + th.getMessage()); + } else { + dsr.matched().response().orElseThrow(() -> new RuntimeException("Failed to deploy")); + System.out.println("Stack created successfully"); + } + }).join(); + } + } else { + System.out.format("Error creating stack: " + t.getMessage(), t); + throw new RuntimeException(t.getCause().getMessage(), t); + } + }).join(); + } else { + logger.info("{} stack already exists", CFN_TEMPLATE); + } + } + + // Check to see if the Stack exists before deploying it + public static Boolean describeStack(String stackName) { + try { + CompletableFuture future = getCloudFormationClient().describeStacks(); + DescribeStacksResponse stacksResponse = (DescribeStacksResponse) future.join(); + List stacks = stacksResponse.stacks(); + for (Stack myStack : stacks) { + if (myStack.stackName().compareTo(stackName) == 0) { + return true; + } + } + } catch (CloudFormationException e) { + System.err.println(e.getMessage()); + } + return false; + } + + public static void destroyCloudFormationStack(String stackName) { + getCloudFormationClient().deleteStack(b -> b.stackName(stackName)) + .whenComplete((dsr, t) -> { + if (dsr != null) { + System.out.println("Delete stack requested ...."); + try (CloudFormationAsyncWaiter waiter = getCloudFormationClient().waiter()) { + waiter.waitUntilStackDeleteComplete(request -> request.stackName(stackName)) + .whenComplete((waiterResponse, throwable) -> + System.out.println("Stack deleted successfully.")) + .join(); + } + } else { + System.out.format("Error deleting stack: " + t.getMessage(), t); + throw new RuntimeException(t.getCause().getMessage(), t); + } + }).join(); + } + + public static CompletableFuture> getStackOutputsAsync(String stackName) { + CloudFormationAsyncClient cloudFormationAsyncClient = getCloudFormationClient(); + + DescribeStacksRequest describeStacksRequest = DescribeStacksRequest.builder() + .stackName(stackName) + .build(); + + return cloudFormationAsyncClient.describeStacks(describeStacksRequest) + .handle((describeStacksResponse, throwable) -> { + if (throwable != null) { + throw new RuntimeException("Failed to get stack outputs for: " + stackName, throwable); + } + + // Process the result + if (describeStacksResponse.stacks().isEmpty()) { + throw new RuntimeException("Stack not found: " + stackName); + } + + Stack stack = describeStacksResponse.stacks().get(0); + Map outputs = new HashMap<>(); + for (Output output : stack.outputs()) { + outputs.put(output.outputKey(), output.outputValue()); + } + + return outputs; + }); + } + + public static void emptyS3Bucket(String bucketName) { + S3AsyncClient s3Client = S3AsyncClient.builder().build(); + + s3Client.listObjectsV2(req -> req.bucket(bucketName)) + .thenCompose(response -> { + List> deleteFutures = response.contents().stream() + .map(s3Object -> s3Client.deleteObject(req -> req + .bucket(bucketName) + .key(s3Object.key()))) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(deleteFutures.toArray(new CompletableFuture[0])); + }) + .join(); + + s3Client.close(); + } +} + diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 2c3cbb3691f..7876abfcf46 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -18,28 +18,30 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; +import software.amazon.awssdk.services.entityresolution.model.JobMetrics; import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; import software.amazon.awssdk.services.entityresolution.model.OutputSource; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; +import software.amazon.awssdk.services.entityresolution.model.SchemaAttributeType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { @@ -59,23 +61,23 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return entityResolutionAsyncClient; } @@ -90,43 +92,43 @@ public static S3AsyncClient getS3AsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); s3AsyncClient = S3AsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return s3AsyncClient; } // snippet-start:[entityres.java2_list_mappings.main] + /** - * Lists the schema mappings associated with the current AWS account. - * This method uses an asynchronous paginator to retrieve the schema mappings, - * and prints the name of each schema mapping to the console. + * Lists the schema mappings associated with the current AWS account. This method uses an asynchronous paginator to + * retrieve the schema mappings, and prints the name of each schema mapping to the console. */ public void ListSchemaMappings() { ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() - .build(); + .build(); ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); - // Iterate through the pages of results + // Iterate through the pages of results CompletableFuture future = paginator.subscribe(response -> { response.schemaList().forEach(schemaMapping -> - logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) + logger.info("Schema Mapping Name: " + schemaMapping.schemaName()) ); }); @@ -136,6 +138,7 @@ public void ListSchemaMappings() { // snippet-end:[entityres.java2_list_mappings.main] // snippet-start:[entityres.java2_delete_matching_workflow.main] + /** * Asynchronously deletes a workflow with the specified name. * @@ -145,20 +148,21 @@ public void ListSchemaMappings() { */ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { DeleteMatchingWorkflowRequest request = DeleteMatchingWorkflowRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().deleteMatchingWorkflow(request) - .thenAccept(response -> { - // No response object, just log success - }) - .exceptionally(exception -> { - throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); - }); + .thenAccept(response -> { + // No response object, just log success + }) + .exceptionally(exception -> { + throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); + }); } // snippet-end:[entityres.java2_delete_matching_workflow.main] // snippet-start:[entityres.java2_create_schema.main] + /** * Creates a schema mapping asynchronously. * @@ -166,184 +170,231 @@ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) * @return a {@link CompletableFuture} that represents the asynchronous creation of the schema mapping */ public CompletableFuture createSchemaMappingAsync(String schemaName) { - List schemaAttributes = List.of( - SchemaInputAttribute.builder().matchKey("id").fieldName("id").type("UNIQUE_ID").build(), - SchemaInputAttribute.builder().matchKey("name").fieldName("name").type("STRING").build(), - SchemaInputAttribute.builder().matchKey("email").fieldName("email").type("STRING").build() - ); + List schemaAttributes = null; + if (schemaName.startsWith("json")) { + schemaAttributes = List.of( + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build() + ); + } else { + schemaAttributes = List.of( + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build(), + SchemaInputAttribute.builder().fieldName("phone").type(SchemaAttributeType.PROVIDER_ID).subType("STRING").build() + ); + } CreateSchemaMappingRequest request = CreateSchemaMappingRequest.builder() - .schemaName(schemaName) - .mappedInputFields(schemaAttributes) - .build(); + .schemaName(schemaName) + .mappedInputFields(schemaAttributes) + .build(); return getResolutionAsyncClient().createSchemaMapping(request) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("Schema Mapping Created Successfully!"); - } else { - throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); - } - }); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("[{}] schema mapping Created Successfully!", schemaName); + } else { + throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_create_schema.main] // snippet-start:[entityres.java2_get_schema_mapping.main] + /** * Retrieves the schema mapping asynchronously. * * @param schemaName the name of the schema to retrieve the mapping for - * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation is complete + * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation + * is complete * @throws RuntimeException if the schema mapping retrieval fails */ public CompletableFuture getSchemaMappingAsync(String schemaName) { GetSchemaMappingRequest mappingRequest = GetSchemaMappingRequest.builder() - .schemaName(schemaName) - .build(); + .schemaName(schemaName) + .build(); return getResolutionAsyncClient().getSchemaMapping(mappingRequest) - .whenComplete((response, exception) -> { - if (response != null) { - response.mappedInputFields().forEach(attribute -> - logger.info("Attribute Name: " + attribute.fieldName() + - ", Attribute Type: " + attribute.type().toString())); - } else { - throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); - } - }); + .whenComplete((response, exception) -> { + if (response != null) { + response.mappedInputFields().forEach(attribute -> + logger.info("Attribute Name: " + attribute.fieldName() + + ", Attribute Type: " + attribute.type().toString())); + } else { + throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_get_schema_mapping.main] // snippet-start:[entityres.java2_get_job.main] + /** * Asynchronously retrieves a matching job based on the provided job ID and workflow name. * - * @param jobId the ID of the job to retrieve - * @param workflowName the name of the workflow associated with the job + * @param jobId the ID of the job to retrieve + * @param workflowName the name of the workflow associated with the job * @return a {@link CompletableFuture} that completes when the job information is available or an exception occurs */ public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenAccept(response -> { - logger.info("Job status: " + response.status()); - logger.info("Job details: " + response.toString()); - }) - .exceptionally(ex -> { - throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); - }); + .thenAccept(response -> { + logger.info("Job status: " + response.status()); + logger.info("Job details: " + response.toString()); + }) + .exceptionally(ex -> { + throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); + }); } // snippet-end:[entityres.java2_get_job.main] // snippet-start:[entityres.java2_start_job.main] + /** * Starts a matching job asynchronously for the specified workflow name. * * @param workflowName the name of the workflow for which to start the matching job - * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty string if the operation fails + * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty + * string if the operation fails */ public CompletableFuture startMatchingJobAsync(String workflowName) { StartMatchingJobRequest jobRequest = StartMatchingJobRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().startMatchingJob(jobRequest) - .whenComplete((response, exception) -> { - if (response != null) { - // Get the job ID from the response - String jobId = response.jobId(); - logger.info("Job ID: " + jobId); - } else { - // Handle the exception if the response is null - throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); - } - }) - .thenApply(response -> response != null ? response.jobId() : ""); + .whenComplete((response, exception) -> { + if (response != null) { + // Get the job ID from the response + String jobId = response.jobId(); + logger.info("Job ID: " + jobId); + } else { + // Handle the exception if the response is null + throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response != null ? response.jobId() : ""); } // snippet-end:[entityres.java2_start_job.main] // snippet-start:[entityres.java2_check_matching_workflow.main] + /** * Checks the status of a workflow asynchronously. * - * @param jobId the ID of the job to check - * @param workflowName the name of the workflow to check - * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed successfully + * @param jobId the ID of the job to check + * @param workflowName the name of the workflow to check + * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed + * successfully */ public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenApply(response -> { - logger.info("\nJob status: " + response.status()); - return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); - }) - .exceptionally(exception -> { - logger.info("Error checking workflow status: " + exception.getMessage()); - return false; - }); + .thenApply(response -> { + logger.info("\nJob status: " + response.status()); + return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); + }) + .exceptionally(exception -> { + logger.info("Error checking workflow status: " + exception.getMessage()); + return false; + }); } // snippet-end:[entityres.java2_check_matching_workflow.main] // snippet-start:[entityres.java2_create_matching_workflow.main] + /** * Creates an asynchronous CompletableFuture to manage the creation of a matching workflow. * * @param roleARN the AWS IAM role ARN to be used for the workflow execution * @param workflowName the name of the workflow to be created * @param outputBucket the S3 bucket path where the workflow output will be stored - * @param inputGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source - * @param schemaName the name of the schema to be used for the input source + * @param jsonGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source + * @param jsonErSchemaMappingName the name of the schema to be used for the input source * @return a CompletableFuture that, when completed, will return the ARN of the created workflow */ - public CompletableFuture createMatchingWorkflowAsync(String roleARN, String workflowName, String outputBucket, String inputGlueTableArn, String schemaName) { - InputSource inputSource = InputSource.builder() - .inputSourceARN(inputGlueTableArn) - .schemaName(schemaName) - .build(); + public CompletableFuture createMatchingWorkflowAsync( + String roleARN + , String workflowName + , String outputBucket + , String jsonGlueTableArn + , String jsonErSchemaMappingName + , String csvGlueTableArn + , String csvErSchemaMappingName) { + + InputSource jsonInputSource = InputSource.builder() + .inputSourceARN(jsonGlueTableArn) + .schemaName(jsonErSchemaMappingName) + .applyNormalization(false) + .build(); + + InputSource csvInputSource = InputSource.builder() + .inputSourceARN(csvGlueTableArn) + .schemaName(csvErSchemaMappingName) + .applyNormalization(false) + .build(); - OutputAttribute outputAttribute = OutputAttribute.builder() - .name("id") - .build(); + OutputAttribute idOutputAttribute = OutputAttribute.builder() + .name("id") + .build(); + + OutputAttribute nameOutputAttribute = OutputAttribute.builder() + .name("name") + .build(); + + OutputAttribute emailOutputAttribute = OutputAttribute.builder() + .name("email") + .build(); + + OutputAttribute phoneOutputAttribute = OutputAttribute.builder() + .name("phone") + .build(); OutputSource outputSource = OutputSource.builder() - .outputS3Path(outputBucket) - .output(outputAttribute) - .build(); + .outputS3Path("s3://" + outputBucket + "/eroutput") + .output(idOutputAttribute, nameOutputAttribute, emailOutputAttribute, phoneOutputAttribute) + .applyNormalization(false) + .build(); - ResolutionTechniques type = ResolutionTechniques.builder() - .resolutionType(ResolutionType.ML_MATCHING) - .build(); + ResolutionTechniques resolutionType = ResolutionTechniques.builder() + .resolutionType(ResolutionType.ML_MATCHING) + .build(); CreateMatchingWorkflowRequest workflowRequest = CreateMatchingWorkflowRequest.builder() - .roleArn(roleARN) - .description("Created by using the AWS SDK for Java") - .workflowName(workflowName) - .inputSourceConfig(List.of(inputSource)) - .outputSourceConfig(List.of(outputSource)) - .resolutionTechniques(type) - .build(); + .roleArn(roleARN) + .description("Created by using the AWS SDK for Java") + .workflowName(workflowName) + .inputSourceConfig(List.of(jsonInputSource, csvInputSource)) + .outputSourceConfig(List.of(outputSource)) + .resolutionTechniques(resolutionType) + .build(); return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("Workflow created successfully."); - } else { - throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); - } - }) - .thenApply(CreateMatchingWorkflowResponse::workflowArn); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Workflow created successfully."); + } else { + throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); + } + }) + .thenApply(CreateMatchingWorkflowResponse::workflowArn); } // snippet-end:[entityres.java2_create_matching_workflow.main] // snippet-start:[entityres.java2_tag_resource.main] + /** * Tags the specified schema mapping ARN. * @@ -355,62 +406,62 @@ public CompletableFuture tagEntityResource(String schemaMappingARN) { tags.put("tag2", "tag2Value"); TagResourceRequest request = TagResourceRequest.builder() - .resourceArn(schemaMappingARN) - .tags(tags) - .build(); + .resourceArn(schemaMappingARN) + .tags(tags) + .build(); return getResolutionAsyncClient().tagResource(request) - .thenAccept(response -> logger.info("Successfully tagged the resource.")) - .exceptionally(exception -> { - throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); - }); + .thenAccept(response -> logger.info("Successfully tagged the resource.")) + .exceptionally(exception -> { + throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); + }); } - // snippet-end:[entityres.java2_tag_resource.main] + public CompletableFuture getJobInfo(String workflowName, String jobId){ + return getResolutionAsyncClient().getMatchingJob(b -> b + .workflowName(workflowName) + .jobId(jobId)) + .thenApply(response -> response.metrics()); + + } /** - * Uploads a local file to an Amazon S3 bucket asynchronously. + * Uploads data to an Amazon S3 bucket asynchronously. * - * @param bucketName the name of the S3 bucket to upload the file to - * @param json the JSON data to be uploaded - * @return a {@link CompletableFuture} representing the asynchronous operation of uploading the file + * @param bucketName the name of the S3 bucket to upload the data to + * @param jsonData the JSON data to be uploaded + * @param csvData the CSV data to be uploaded + * @return a {@link CompletableFuture} representing both asynchronous operation of uploading the data * @throws RuntimeException if an error occurs during the file upload */ - public CompletableFuture uploadLocalFileAsync(String bucketName, String json) { - - String key = "data/data.json"; // Corrected: No leading "/" - PutObjectRequest objectRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(key) - .contentType("application/json") - .build(); - - CompletableFuture response = getS3AsyncClient().putObject(objectRequest, AsyncRequestBody.fromString(json)); - return response.whenComplete((resp, ex) -> { - if (ex != null) { - throw new RuntimeException("Failed to upload file", ex); - } - }); - } - /** - * Checks if a specific object exists in an Amazon S3 bucket. - * - * @param bucketName the name of the S3 bucket to check - * @return true if the object exists, false otherwise - */ - public boolean doesObjectExist(String bucketName) { - try { - String key = "data/data.json"; - getS3AsyncClient().headObject(HeadObjectRequest.builder() + public void uploadInputData(String bucketName, String jsonData, String csvData) { + // Upload JSON data. + String jsonKey = "jsonData/data.json"; + PutObjectRequest jsonUploadRequest = PutObjectRequest.builder() .bucket(bucketName) - .key(key) - .build()); - return true; // File exists + .key(jsonKey) + .contentType("application/json") + .build(); + + CompletableFuture jsonUploadResponse = getS3AsyncClient().putObject(jsonUploadRequest, AsyncRequestBody.fromString(jsonData)); + + // Upload CSV data. + String csvKey = "csvData/data.csv"; + PutObjectRequest csvUploadRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(csvKey) + .contentType("text/csv") + .build(); + CompletableFuture csvUploadResponse = getS3AsyncClient().putObject(csvUploadRequest, AsyncRequestBody.fromString(csvData)); + + CompletableFuture.allOf(jsonUploadResponse, csvUploadResponse) + .whenComplete((result, ex) -> { + if (ex != null) { + throw new RuntimeException("Failed to upload files", ex); + } + }).join(); - } catch (S3Exception e) { - return false; - } } -} -// snippet-end:[entityres.java2_actions.main] \ No newline at end of file +// snippet-end:[entityres.java2_actions.main] +} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 6099da2d4c5..a0551a628a9 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -8,43 +8,34 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.JobMetrics; + +import java.util.Map; import java.util.Scanner; +import java.util.UUID; import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_scenario.main] public class EntityResScenario { private static final Logger logger = LoggerFactory.getLogger(EntityResScenario.class); public static final String DASHES = new String(new char[80]).replace("\0", "-"); - public static void main(String[] args) throws InterruptedException { - - final String usage = """ - - Usage: - + private static final String STACK_NAME = "EntityResolutionCdkStack"; + private static final String ENTITY_RESOLUTION_ROLE_ARN_KEY = "EntityResolutionRoleArn"; + private static final String GLUE_DATA_BUCKET_NAME_KEY = "GlueDataBucketName"; + private static final String JSON_GLUE_TABLE_ARN_KEY = "JsonErGlueTableArn"; + private static final String CSV_GLUE_TABLE_ARN_KEY = "CsvErGlueTableArn"; + private static String glueBucketName; + private static String workflowName = "workflow-"+ UUID.randomUUID(); - Where: - workflowName - A unique identifier for the matching workflow, used in the entity resolution process. - schemaName - The name of the schema, which defines the structure and attributes for the data being processed. - roleARN: The ARN of the IAM role, that grants permissions for the entity resolution workflow (this resource is created using the CDK script. See the Readme). - dataS3bucket: The S3 bucket,that stores the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. - outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. - inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. - """; - - // if (args.length != 6) { - // logger.info(usage); - // return; - // } - - String workflowName = "workflow100" ; //args[0]; - String schemaName = "schemaName100" ;//args[1]; + public static void main(String[] args) throws InterruptedException { - // Use the AWS CDK to create these AWS resources. - // See the Readme file located at resources/cdk/entityresolution_resources. - String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm" ; //args[2]; - String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d" ; //args[3]; - String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; //args[4]; - String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution" ; //args[5]; + String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); + String jsonSchemaMappingArn = null; + String csvSchemaMappingName = "csv-" + UUID.randomUUID(); + String csvSchemaMappingArn = null; + String roleARN; + String csvGlueTableArn; + String jsonGlueTableArn; EntityResActions actions = new EntityResActions(); Scanner scanner = new Scanner(System.in); @@ -78,6 +69,26 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); + logger.info(""" + To prepare the AWS resources needed for this scenario application, the next step uploads + a CloudFormation template whose resulting stack creates the following resources: + - An AWS Glue Data Catalog table + - An AWS IAM role + - An AWS S3 bucket + - An AWS Entity Resolution Schema + + It can take a couple minutes for the Stack to finish creating the resources. + """); + waitForInputToContinue(scanner); + logger.info("Generating resources..."); + CloudFormationHelper.deployCloudFormationStack(STACK_NAME); + Map outputsMap = CloudFormationHelper.getStackOutputsAsync(STACK_NAME).join(); + roleARN = outputsMap.get(ENTITY_RESOLUTION_ROLE_ARN_KEY); + glueBucketName = outputsMap.get(GLUE_DATA_BUCKET_NAME_KEY); + csvGlueTableArn = outputsMap.get(CSV_GLUE_TABLE_ARN_KEY); + jsonGlueTableArn = outputsMap.get(JSON_GLUE_TABLE_ARN_KEY); + logger.info(DASHES); + waitForInputToContinue(scanner); /* This JSON is a valid input for the AWS Entity Resolution service. The JSON represents an array of three objects, each containing an "id", "name", and "email" @@ -85,57 +96,57 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Entity Resolution service. */ String json = """ - [ - { - "id": "1", - "name": "Alice Johnson", - "email": "alice.johnson@example.com" - }, - { - "id": "2", - "name": "Bob Smith", - "email": "bob.smith@example.com" - }, - { - "id": "3", - "name": "Charlie Black", - "email": "charlie.black@example.com" - } - ] - """; - logger.info("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); + {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} + {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} + {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} + """; + logger.info("Upload the following JSON objects to the {} S3 bucket.", glueBucketName); logger.info(json); + String csv = """ + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + """; + logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); + logger.info(csv); waitForInputToContinue(scanner); - if (!actions.doesObjectExist(dataS3bucket)) { - actions.uploadLocalFileAsync(dataS3bucket, json); - } else { - logger.info("The JSON exists in " + dataS3bucket); - } + actions.uploadInputData(glueBucketName, json, csv); + logger.info("The JSON objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("1. Create Schema Mapping"); logger.info(""" - Entity Resolution Schema Mapping aligns and integrates data from + Entity Resolution schema mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - In this example, the schema mapping lines up with the fields in the JSON. That is, + In this example, the schema mapping lines up with the fields in the JSON objects. That is, it contains these fields: id, name, and email. """); waitForInputToContinue(scanner); - String mappingARN = null; try { - CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); - mappingARN = response.schemaArn(); + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); + jsonSchemaMappingArn = response.schemaArn(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } + try { + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); + csvSchemaMappingArn = response.schemaArn(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + + waitForInputToContinue(scanner); logger.info(DASHES); @@ -148,11 +159,14 @@ Amazon Web Services (AWS) that helps organizations extract, link, and data profiling, and machine learning algorithms, it evaluates attributes like names or emails to detect duplicates or relationships, even with variations or inconsistencies. - The workflow outputs consolidated, de-duplicated data,\s + The workflow outputs consolidated, de-duplicated data. + + We will use the machine learning-based matching technique. """); waitForInputToContinue(scanner); try { - String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn + , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); @@ -175,7 +189,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("4. Get details for job "+jobId); + logger.info("4. While the matching job is running, let's look at other API methods. First, let's get details for job "+jobId); waitForInputToContinue(scanner); try { actions.getMatchingJobAsync(jobId, workflowName).join(); @@ -186,10 +200,10 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("5. Get Schema Mapping."); + logger.info("5. Get the schema mapping for the JSON data."); waitForInputToContinue(scanner); try { - actions.getSchemaMappingAsync(schemaName).join(); + actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); logger.info("Schema mapping retrieval completed."); } catch (CompletionException ce) { logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); @@ -204,7 +218,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("7. Tag the "+schemaName +"resource."); + logger.info("7. Tag the {} resource.", jsonSchemaMappingName); logger.info(""" Tags can help you organize and categorize your Entity Resolution resources. You can also use them to scope user permissions by granting a user permission @@ -212,7 +226,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, the SchemaMapping is tagged. """); - actions.tagEntityResource(mappingARN).join(); + actions.tagEntityResource(jsonSchemaMappingArn).join(); waitForInputToContinue(scanner); logger.info(DASHES); @@ -220,7 +234,11 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("8. Delete the AWS Entity Resolution Workflow."); logger.info(""" You cannot delete a workflow that is in a running state. - Would you like to wait for the workflow to complete. + Would you like to wait for the workflow that we started in step 3 to complete. + + If you choose not to wait, you will need to delete the workflow manually + in the AWS Management Console. + This can take up to 30 mins (y/n). """); String delAns = scanner.nextLine().trim(); @@ -228,6 +246,32 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("You selected to delete Entity Resolution Workflow."); waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); + logger.info("Number of input records: {}", metrics.inputRecords()); + logger.info("Number of match ids: {}", metrics.matchIDs()); + logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); + logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); + logger.info(""" + + The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + + Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; + For example 'Bob Smith Jr.' compared to 'Bob Smith'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, + the confidence level is lower for the differing email addresses. + + """); try { actions.deleteMatchingWorkflowAsync(workflowName).join(); logger.info("Workflow deleted successfully!"); @@ -239,6 +283,20 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); logger.info(DASHES); + logger.info(DASHES); + logger.info(""" + Now we delete the CloudFormation stack, which deletes + the resources that were created at the beginning + """); + waitForInputToContinue(scanner); + logger.info(DASHES); + try { + deleteResources(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); + } + logger.info(DASHES); logger.info("This concludes the AWS Entity Resolution scenario."); logger.info(DASHES); @@ -290,5 +348,11 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } } + private static void deleteResources(){ + CloudFormationHelper.emptyS3Bucket(glueBucketName); + CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); + logger.info("Resources deleted successfully!"); + + } } // snippet-end:[entityres.java2_scenario.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/TODO.md b/javav2/example_code/entityresolution/src/main/resources/TODO.md new file mode 100644 index 00000000000..8e3963dca2a --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/TODO.md @@ -0,0 +1,8 @@ +# Suggestions to improve the scenario + +Need to delete the schema mapping when you delete the workflow. + +Use two input data sources, since that is what a customer would do at a minimum. The input data for the scenario should contain records that do +and don't match. Make the second data source in CSV. + +When the job completes, display the results from the S3 bucket--both success and error. \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/data.csv b/javav2/example_code/entityresolution/src/main/resources/data.csv new file mode 100644 index 00000000000..3ec062e335d --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/data.csv @@ -0,0 +1,5 @@ + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/data.json b/javav2/example_code/entityresolution/src/main/resources/data.json new file mode 100644 index 00000000000..0375ab4e2be --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/data.json @@ -0,0 +1,3 @@ +{"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} +{"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} +{"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/glue.yaml b/javav2/example_code/entityresolution/src/main/resources/template.yaml similarity index 72% rename from javav2/example_code/entityresolution/src/main/resources/glue.yaml rename to javav2/example_code/entityresolution/src/main/resources/template.yaml index d09227c86fe..f0395929fa7 100644 --- a/javav2/example_code/entityresolution/src/main/resources/glue.yaml +++ b/javav2/example_code/entityresolution/src/main/resources/template.yaml @@ -1,14 +1,12 @@ Resources: - GlueDataBucket278CFAC6: + ErBucket6EA35F9D: Type: AWS::S3::Bucket Properties: - BucketName: glue-2cf5649393c7465f926ae00d0592eba8 - VersioningConfiguration: - Status: Enabled - UpdateReplacePolicy: Retain - DeletionPolicy: Retain + BucketName: erbucketf684533d2680435fa99d24b1bdaf5179 + UpdateReplacePolicy: Delete + DeletionPolicy: Delete Metadata: - aws:cdk:path: EntityResolutionCdkStack/GlueDataBucket/Resource + aws:cdk:path: EntityResolutionCdkStack/ErBucket/Resource GlueDatabase: Type: AWS::Glue::Database Properties: @@ -18,7 +16,7 @@ Resources: Name: entity_resolution_db Metadata: aws:cdk:path: EntityResolutionCdkStack/GlueDatabase - GlueTable: + jsongluetable: Type: AWS::Glue::Table Properties: CatalogId: @@ -26,7 +24,7 @@ Resources: DatabaseName: Ref: GlueDatabase TableInput: - Name: entity_resolution + Name: jsongluetable StorageDescriptor: Columns: - Name: id @@ -40,8 +38,8 @@ Resources: Fn::Join: - "" - - s3:// - - Ref: GlueDataBucket278CFAC6 - - /data/ + - Ref: ErBucket6EA35F9D + - /jsonData/ OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat SerdeInfo: Parameters: @@ -51,7 +49,43 @@ Resources: DependsOn: - GlueDatabase Metadata: - aws:cdk:path: EntityResolutionCdkStack/GlueTable + aws:cdk:path: EntityResolutionCdkStack/jsongluetable + csvgluetable: + Type: AWS::Glue::Table + Properties: + CatalogId: + Ref: AWS::AccountId + DatabaseName: + Ref: GlueDatabase + TableInput: + Name: csvgluetable + StorageDescriptor: + Columns: + - Name: id + Type: string + - Name: name + Type: string + - Name: email + Type: string + - Name: phone + Type: string + InputFormat: org.apache.hadoop.mapred.TextInputFormat + Location: + Fn::Join: + - "" + - - s3:// + - Ref: ErBucket6EA35F9D + - /csvData/ + OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat + SerdeInfo: + Parameters: + serialization.format: "1" + SerializationLibrary: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe + TableType: EXTERNAL_TABLE + DependsOn: + - GlueDatabase + Metadata: + aws:cdk:path: EntityResolutionCdkStack/csvgluetable EntityResolutionRoleB51A51D3: Type: AWS::IAM::Role Properties: @@ -101,32 +135,34 @@ Resources: - Ref: EntityResolutionRoleB51A51D3 Metadata: aws:cdk:path: EntityResolutionCdkStack/EntityResolutionRole/DefaultPolicy/Resource - OutputBucket7114EB27: - Type: AWS::S3::Bucket - Properties: - BucketName: entity-resolution-output-entityresolutioncdkstack - VersioningConfiguration: - Status: Enabled - UpdateReplacePolicy: Retain - DeletionPolicy: Retain - Metadata: - aws:cdk:path: EntityResolutionCdkStack/OutputBucket/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: - Analytics: v2:deflate64:H4sIAAAAAAAA/02MMQ+CMBSEfwt7eYLEuFsnFwy6m0cp5klpDW0lpul/N7SL0919d7k91M0BqgJXW4phKhX1EG4OxcRwtY9gGwgnLybpGB91dpE9lZcQ+KjP6LBHK7fyjr2SkRHOEDqjEkt6NYrEd4vZxcg6aY1fRNq03r19uv+n3OiBHBkd2QU/uKuPUEHdFC9LVC5eO5oldFl/L3LHkcUAAAA= + Analytics: v2:deflate64:H4sIAAAAAAAA/02MzQ7CIBCEn6V3WPuTvoD15EVTvZstRbOWgimgMYR3t4WLp5n5ZjI1VE0LZYEfy8U4cUUDhItDMbEV3YJtIOy9mKRj3V1nF9lDeQlhBQd0OKCVW3nFQcnICGcIvVGJJT0bReK7xexiZL20xi8ibU7evXy6/6ed0SM5MjqyI75xV1dQQls8LRFfvHY0S+iz/gCPIXoRxAAAAA== Metadata: aws:cdk:path: EntityResolutionCdkStack/CDKMetadata/Default Condition: CDKMetadataAvailable Outputs: - EntityResolutionArn: - Description: The ARN of the Glue Role + EntityResolutionRoleArn: + Description: The ARN of the EntityResolution Role Value: Fn::GetAtt: - EntityResolutionRoleB51A51D3 - Arn - GlueTableArn: - Description: The ARN of the Glue Table + JsonErGlueTableArn: + Description: The ARN of the Json Glue Table + Value: + Fn::Join: + - "" + - - "arn:aws:glue:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :table/ + - Ref: GlueDatabase + - /jsongluetable + CsvErGlueTableArn: + Description: The ARN of the CSV Glue Table Value: Fn::Join: - "" @@ -136,11 +172,11 @@ Outputs: - Ref: AWS::AccountId - :table/ - Ref: GlueDatabase - - /entity_resolution + - /csvgluetable GlueDataBucketName: Description: The name of the Glue Data Bucket Value: - Ref: GlueDataBucket278CFAC6 + Ref: ErBucket6EA35F9D Conditions: CDKMetadataAvailable: Fn::Or: @@ -224,17 +260,4 @@ Parameters: Type: AWS::SSM::Parameter::Value Default: /cdk-bootstrap/hnb659fds/version Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] -Rules: - CheckBootstrapVersion: - Assertions: - - Assert: - Fn::Not: - - Fn::Contains: - - - "1" - - "2" - - "3" - - "4" - - "5" - - Ref: BootstrapVersion - AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI. diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index 5e915b8f0ad..b1bb546d8b9 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; @@ -77,7 +76,7 @@ public static void setUp() { ] """; if (!actions.doesObjectExist(dataS3bucket)) { - actions.uploadLocalFileAsync(dataS3bucket, json); + actions.uploadInputData(dataS3bucket, json); } else { System.out.println("The JSON exists in " + dataS3bucket); } diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md index 262d244b901..a686e715048 100644 --- a/resources/cdk/entityresolution_resources/README.md +++ b/resources/cdk/entityresolution_resources/README.md @@ -1,8 +1,9 @@ -# AWS Entity Resolution resources +# AWS Entity Resolution scenario resources ## Overview -Creates the following AWS resources for the AWS Entity Resolution scenario: +This AWS CDK Java application generates a AWS CloudFormation template. +The CloudFormation template creates the following resources for the AWS Entity Resolution scenario application: * An AWS IAM role that has permissions required to run this Scenario. * An AWS Glue table that provides the input data for the entity resolution matching workflow. @@ -11,50 +12,35 @@ Creates the following AWS resources for the AWS Entity Resolution scenario: ## ⚠️ Important -* Running this code might result in charges to your AWS account. +* When the template is used by the AWS Entity Resolution scenario application, + the resources it creates might result in charges to your account. * This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). -## Deploy resources - -You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Interface -(AWS CLI) to deploy and destroy the resources for this example. - -### Deploy with the AWS CDK - -To deploy with the AWS CDK, you must install [Java JDK 17](https://www.oracle.com/ca-en/java/technologies/downloads/) and the -[AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). - -This example was built and tested with AWS CDK 2.135.0. - -Deploy AWS resources by running the following at a command prompt in this README's folder: +## Create a CloudFormation template +To output a template that creates the CloudFormation stack, execute the following CDK CLI command from the +`resources/cdk/entityresolution_resources` working directory: ``` -cdk deploy +cdk synth --yaml > ../../../javav2/example_code/entityresolution/src/main/resources/template.yaml ``` +The result of running this command puts the `template.yaml` file into the directory where +the scenario application can use it. -The stack takes a few minutes to deploy. When it completes, it prints output like -the following: +## Outputs generated +When the template is used and the stack is created by the AWS Entity Resolution scenario application, +the following outputs are generated and used in the application: ``` -Outputs: EntityResolutionCdkStack.EntityResolutionArn = arn:aws:iam::XXXXX:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm EntityResolutionCdkStack.GlueDataBucketName = glue-XXXXX3196d EntityResolutionCdkStack.GlueTableArn = arn:aws:glue:us-east-1:XXXXX:table/entity_resolution_db/entity_resolution ``` -Note - Copy these AWS resources into your AWS Entity Resolution scenario. These values are required for the program to successfully run. +## How stack-created resources are destroyed +AWS Entity Resolution scenario application destroys the resources created by the stack before it completes. -## Destroy resources - -### Destroy with the AWS CDK - -You can use the AWS CDK to destroy the resources by running the following: - -``` -cdk destroy -``` -## Additional resources +## Additional information * [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) * [AWS CLI User Guide for Version 2](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) diff --git a/resources/cdk/entityresolution_resources/cdk.json b/resources/cdk/entityresolution_resources/cdk.json new file mode 100644 index 00000000000..723b2f18b0c --- /dev/null +++ b/resources/cdk/entityresolution_resources/cdk.json @@ -0,0 +1,68 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + } +} diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java index 4aecc53409a..d46eeddf636 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -3,10 +3,18 @@ package com.myorg; -import software.amazon.awscdk.*; -import software.amazon.awscdk.services.iam.*; -import software.amazon.awscdk.services.s3.*; -import software.amazon.awscdk.services.glue.*; +import software.amazon.awscdk.CfnOutput; +import software.amazon.awscdk.CfnOutputProps; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.glue.CfnDatabase; +import software.amazon.awscdk.services.glue.CfnTable; +import software.amazon.awscdk.services.iam.ManagedPolicy; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.iam.ServicePrincipal; +import software.amazon.awscdk.services.s3.Bucket; import software.constructs.Construct; import java.util.List; @@ -20,13 +28,17 @@ public EntityResolutionCdkStack(final Construct scope, final String id) { public EntityResolutionCdkStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); + final String jsonGlueTableName = "jsongluetable"; + final String csvGlueTableName = "csvgluetable"; // 1. Create an S3 bucket for the Glue Data Table String uniqueId = UUID.randomUUID().toString().replace("-", ""); // Remove dashes to ensure compatibility - Bucket glueDataBucket = Bucket.Builder.create(this, "GlueDataBucket") - .bucketName("glue-" + uniqueId) - .versioned(true) - .build(); + + Bucket erBucket = Bucket.Builder.create(this, "ErBucket") + .bucketName("erbucket" + uniqueId) + .versioned(false) + .removalPolicy(RemovalPolicy.DESTROY) + .build(); // 2. Create a Glue database CfnDatabase glueDatabase = CfnDatabase.Builder.create(this, "GlueDatabase") @@ -37,7 +49,7 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build(); // 3. Create a Glue table referencing the S3 bucket - CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") +/* CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") .catalogId(this.getAccount()) .databaseName(glueDatabase.getRef()) // Ensure Glue Table references the database correctly .tableInput(CfnTable.TableInputProperty.builder() @@ -58,10 +70,26 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build()) .build()) .build()) - .build(); + .build();*/ + final CfnTable jsonErGlueTable = createGlueTable(jsonGlueTableName + , jsonGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string") + , "s3://" + erBucket.getBucketName() + "/jsonData/" + , "org.openx.data.jsonserde.JsonSerDe"); + + // Ensure Glue Table is created after the Database + jsonErGlueTable.addDependency(glueDatabase); + + final CfnTable csvErGlueTable = createGlueTable(csvGlueTableName + , csvGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string", "phone", "string") + , "s3://" + erBucket.getBucketName() + "/csvData/" + , "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe"); // Ensure Glue Table is created after the Database - glueTable.addDependency(glueDatabase); + csvErGlueTable.addDependency(glueDatabase); // 4. Create an IAM Role for AWS Entity Resolution Role entityResolutionRole = Role.Builder.create(this, "EntityResolutionRole") @@ -74,6 +102,11 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St )) .build(); + new CfnOutput(this, "EntityResolutionRoleArn", CfnOutputProps.builder() + .value(entityResolutionRole.getRoleArn()) + .description("The ARN of the EntityResolution Role") + .build()); + // Add custom permissions for Entity Resolution entityResolutionRole.addToPolicy(PolicyStatement.Builder.create() .actions(List.of( @@ -83,35 +116,58 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .resources(List.of("*")) // Adjust permissions if needed .build()); - // 5. Create an S3 bucket for output data - Bucket outputBucket = Bucket.Builder.create(this, "OutputBucket") - .bucketName("entity-resolution-output-" + id.toLowerCase()) - .versioned(true) - .build(); - - // 6. Output the Role ARN - new CfnOutput(this, "EntityResolutionArn", CfnOutputProps.builder() - .value(entityResolutionRole.getRoleArn()) - .description("The ARN of the Glue Role") + // ------------------------ OUTPUTS -------------------------------------- + new CfnOutput(this, "JsonErGlueTableArn", CfnOutputProps.builder() + .value(createGlueTableArn(jsonErGlueTable, jsonGlueTableName)) + .description("The ARN of the Json Glue Table") .build()); - // 7. Construct and output the Glue Table ARN - String glueTableArn = String.format("arn:aws:glue:%s:%s:table/%s/%s", - this.getRegion(), // Region where the stack is deployed - this.getAccount(), // AWS account ID - glueDatabase.getRef(), // Glue database name (resolved reference) - "entity_resolution" // Corrected table name - ); - - new CfnOutput(this, "GlueTableArn", CfnOutputProps.builder() - .value(glueTableArn) - .description("The ARN of the Glue Table") - .build()); + new CfnOutput(this, "CsvErGlueTableArn", CfnOutputProps.builder() + .value(createGlueTableArn(csvErGlueTable, csvGlueTableName)) + .description("The ARN of the CSV Glue Table") + .build()); - // 8. Output the name of the Glue Data Bucket new CfnOutput(this, "GlueDataBucketName", CfnOutputProps.builder() - .value(glueDataBucket.getBucketName()) // Outputs the bucket name + .value(erBucket.getBucketName()) // Outputs the bucket name .description("The name of the Glue Data Bucket") .build()); } + + CfnTable createGlueTable(String id, String tableName, String databaseRef, Map schemaMap, String dataLocation, String serializationLib){ + return CfnTable.Builder.create(this, id) + .catalogId(this.getAccount()) + .databaseName(databaseRef) // Ensure Glue Table references the database correctly + .tableInput(CfnTable.TableInputProperty.builder() + .name(tableName) // Fixed table name reference + .tableType("EXTERNAL_TABLE") + .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() + .columns(createColumns(schemaMap)) + .location(dataLocation) // Append subpath for data + .inputFormat("org.apache.hadoop.mapred.TextInputFormat") + .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") + .serdeInfo(CfnTable.SerdeInfoProperty.builder() + .serializationLibrary(serializationLib) // Set JSON SerDe + .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON + .build()) + .build()) + .build()) + .build(); + } + List createColumns(Map schemaMap) { + return schemaMap.entrySet().stream() + .map(entry -> CfnTable.ColumnProperty.builder() + .name(entry.getKey()) + .type(entry.getValue()) + .build()) + .toList(); + } + + String createGlueTableArn(CfnTable glueTable, String glueTableName) { + return String.format("arn:aws:glue:%s:%s:table/%s/%s" + , this.getRegion() + , this.getAccount() + , glueTable.getDatabaseName() + , glueTableName + ); + } } diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 6099ece90f5..6e79288f02e 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -3,24 +3,26 @@ This AWS Entity Resolution basic scenario demonstrates how to interact with the ## Key Operations -1. **Create an AWS Entity Resolution Schema Mapping**: - - This step creates an AWS Entity Resolution Schema Mapping by invoking the `createSchemaMapping` method. +1. **Create an AWS Entity Resolution schema mapping**: + - This step creates an AWS Entity Resolution schema mapping by invoking the `createSchemaMapping` method. -2. **Create an AWS Entity Resolution Workflow**: - - This step creates an AWS Entity Resolution matching Workflow by invoking the `createMatchingWorkflow` method. +2. **Create an AWS Entity Resolution workflow**: + - This step creates an AWS Entity Resolution matching workflow by invoking the `createMatchingWorkflow` method. -3. **Start Matching Workflow**: - - This step starts the AWS Entity Resolution matching Workflow by invoking the `startMatchingJob` method. +3. **Start a matching aorkflow**: + - This step starts the AWS Entity Resolution matching workflow by invoking the `startMatchingJob` method. -4. **Get Workflow Job Details**: - - This step gets workflow job details by `getMatchingJob` method. +4. **Get workflow job details**: + - This step gets workflow job details by invoking the `getMatchingJob` method. -**Note** See the Eng spec for a full listing of operations. +**Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. See the Readme file at resources/cdk/entityresolution_resources. +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, +an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. +See the resources [Readme](../../../resources/cdk/entityresolution_resources/README.md) file. ## Implementations diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 990a899296a..f7c4c616544 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -1,62 +1,88 @@ -# AWS Entity Resolution Service Scenario Specification +# Specification for the AWS Entity Resolution Service Scenario ## Overview -This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution using an AWS SDK. It demonstrates various tasks such as creating a Schema Mapping, creating an matching workflow, starting the workflow, and so on. Finally this scenario demonstrates how to clean up resources. Its purpose is to demonstrate how to get up and running with AWS Entity Resolution and an AWS SDK. + +This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution +using an AWS SDK. It demonstrates various tasks such as creating a schema +mapping, creating an matching workflow, starting a workflow, and so on. Finally, +this scenario demonstrates how to clean up resources. ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database and a table, and two S3 buckets. A CDK script is provided to create these resources. + +This Basics scenario requires an IAM role that has permissions to work with the +AWS Entity Resolution service, an AWS Glue database and a table, and two S3 +buckets. +A [CDK script](../../../resources/cdk/entityresolution_resources/README.md +) is provided to create these resources. ## Hello AWS Entity Resolution -This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listMatchingWorkflowsPaginator` to demonstrate how you can read through workflow information. + +This program is intended for users not familiar with the AWS Entity Resolution +Service to easily get up and running. The program uses a +`listMatchingWorkflowsPaginator` to demonstrate how you can read through +workflow information. ## Basics Scenario Program Flow + The AWS Entity Resolution Basics scenario executes the following operations. -1. **Create a Schema Mapping**: - - Description: Creates a schema mapping invoking the `createSchemaMapping` method. - - Exception Handling: Check to see if a `ConflictException` is thrown. - If it is thrown, display the information and end the program. +1. **Create a schema mapping**: + - Description: Creates a schema mapping by invoking the + `createSchemaMapping` method. + - Exception Handling: Check to see if a `ConflictException` is thrown, which + indicates that the schema mapping already exists. If the exception is + thrown, display the information and end the program. 2. **Create a Matching Workflow**: - - Description: Creates a new matching workflow, defining how entities should be resolved and matched.. - - The method `createMatchingWorkflow` is called. - - Exception Handling: Check to see if a `ConflictException` is thrown if a conflict in the current state of the resource exists. If so, - display the message and end the program. + - Description: Creates a new matching workflow that defines how entities + should be resolved and matched. The method `createMatchingWorkflow` is + called. + - Exception Handling: Check to see if a `ConflictException` is thrown, which + is thrown if the matching workflow already exists. If so, display the + message and end the program. 3. **Start Matching Workflow**: - - Description: Initiates a matching workflow to process entity resolution based on predefined configurations. - - The method `startMatchingJob` is called to start the matching workflow. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + - Description: Initiates a matching workflow by calling the + `startMatchingJob` method to process entity resolution based on predefined + configurations. + - Exception Handling: Check to see if an `ConflictException` is thrown, + which indicates that the matching workflow job is already running. If the + exception is thrown, display the message and end the program. 4. **Get Workflow Job Details**: - - Description: Retrieves details about a specific matching workflow job. - - This step uses the method `getMatchingJob`. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. - + - Description: Retrieves details about a specific matching workflow job by + calling the `getMatchingJob` method. + - Exception Handling: Check to see if an `ResourceNotFoundException` is + thrown, which indicates that the workflow cannot be found. If the + exception is thrown, display the message and end the program. 5. **List Matching Workflows**: - - Description: Lists all matching workflows created within the account. - - This step uses the method `listMatchingWorkflows`. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + - Description: Lists all matching workflows created within the account by + calling the `listMatchingWorkflows` method. + - Exception Handling: Check to see if an `CompletionException` is thrown. If + so, display the message and end the program. 6. **Get Schema Mapping**: - - Description: Lists all schema mappings available in the account. - - The method `createPortal` is called. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program + - Description: Returns the `SchemaMapping` of a given name by calling the + `getSchemaMapping` method. + - Exception Handling: Check to see if a `ResourceNotFoundException` is + thrown. If so, display the message and end the program. 7. **Tag Resource**: - - Description: Adds tags associated with an AWS Entity Resolution resource. - - The method `tagResource` is called. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program - -8. **Delete Matching Workflow**: - - Description: Deletes a specified matching workflowy. - - The methods `deleteMatchingWorkflow` is called. - - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program - + - Description: Adds tags associated with an AWS Entity Resolution resource + by calling the`tagResource` method. + - Exception Handling: Check to see if a `ResourceNotFoundException` is + thrown. If so, display the message and end the program +8. **Delete Matching Workflow**: + - Description: Deletes a specified matching workflow by calling the + `deleteMatchingWorkflow` method. + - Exception Handling: Check to see if an `ConflictException` is thrown. If + so, display the message and end the program. ### Program execution -The following shows the output of the AWS Entity Resolution Basics scenario in the console. + +The following shows the output of the AWS Entity Resolution Basics scenario in +the console. ``` Welcome to the AWS Entity Resolution Scenario. @@ -243,19 +269,20 @@ This concludes the AWS Entity Resolution scenario. ## SOS Tags The following table describes the metadata used in this Basics Scenario. -| action | metadata file | metadata key | -|-------------------------|------------------------|---------------------------------------- | -| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | -| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | -| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | -| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | -| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | -| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | -| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | -| `tagResource ` | entity_metadata.yaml | entity_TagResource | -| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | -| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | -| `scenario` | entity_metadata.yaml | entity_Scenario | + +| action | metadata file | metadata key | +|-------------------------|------------------------|-------------------------------| +| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | +| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | +| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | +| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | +| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | +| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | +| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | +| `tagResource ` | entity_metadata.yaml | entity_TagResource | +| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | +| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | +| `scenario` | entity_metadata.yaml | entity_Scenario | From 20f37f3fdd195f8ac87e45e9954883a3e237aea9 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 24 Feb 2025 16:20:57 -0500 Subject: [PATCH 066/144] updated validation file --- .doc_gen/validation.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.doc_gen/validation.yaml b/.doc_gen/validation.yaml index 3aadb80233f..1e1718ecab5 100644 --- a/.doc_gen/validation.yaml +++ b/.doc_gen/validation.yaml @@ -1,6 +1,7 @@ allow_list: # Git commits - "cd5e746ec203c8c3c61647e0886a8df8c1e78e41" + - "erbucketf684533d2680435fa99d24b1bdaf5179" - "725feb26d6f73bc1d83dbbe075ae8ea991efb245" - "e9772d140489982e0e3704fea5ee93d536f1e275" # Safe look-alikes, mostly tokens and paths that happen to be 40 characters. From 77082d6785ea846c2d8ea2d5abc7156eff5067c5 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 24 Feb 2025 16:24:17 -0500 Subject: [PATCH 067/144] updated validation file --- .../com/myorg/EntityResolutionCdkTest.java | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java diff --git a/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java b/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java deleted file mode 100644 index ac832c96bdb..00000000000 --- a/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java +++ /dev/null @@ -1,26 +0,0 @@ -// package com.myorg; - -// import software.amazon.awscdk.App; -// import software.amazon.awscdk.assertions.Template; -// import java.io.IOException; - -// import java.util.HashMap; - -// import org.junit.jupiter.api.Test; - -// example test. To run these tests, uncomment this file, along with the -// example resource in java/src/main/java/com/myorg/EntityResolutionCdkStack.java -// public class EntityResolutionCdkTest { - -// @Test -// public void testStack() throws IOException { -// App app = new App(); -// EntityResolutionCdkStack stack = new EntityResolutionCdkStack(app, "test"); - -// Template template = Template.fromStack(stack); - -// template.hasResourceProperties("AWS::SQS::Queue", new HashMap() {{ -// put("VisibilityTimeout", 300); -// }}); -// } -// } From bafa6bfaf265a38d3b64b224bc9d9bee9b9aa76c Mon Sep 17 00:00:00 2001 From: David Souther Date: Mon, 24 Feb 2025 16:56:43 -0500 Subject: [PATCH 068/144] Remove add_services metadata (#7258) This is only used inconsistently in cross_metadata, and will be simplified if we remove it. --- .doc_gen/metadata/cross_metadata.yaml | 107 ++++++++++++------ python/example_code/cloudwatch-logs/README.md | 13 +++ python/example_code/dynamodb/README.md | 28 ++++- python/example_code/s3/README.md | 15 ++- python/example_code/ses/README.md | 15 ++- python/example_code/sns/README.md | 41 ++++++- python/example_code/sqs/README.md | 15 ++- 7 files changed, 193 insertions(+), 41 deletions(-) diff --git a/.doc_gen/metadata/cross_metadata.yaml b/.doc_gen/metadata/cross_metadata.yaml index db05286b430..01931cd3ea2 100644 --- a/.doc_gen/metadata/cross_metadata.yaml +++ b/.doc_gen/metadata/cross_metadata.yaml @@ -16,7 +16,8 @@ cross_MessageProcessingFrameworkTutorial: cross_FSA: title: Create an application that analyzes customer feedback and synthesizes audio title_abbrev: Create an application to analyze customer feedback - synopsis: create an application that analyzes customer comment cards, translates them from their original language, determines + synopsis: + create an application that analyzes customer comment cards, translates them from their original language, determines their sentiment, and generates an audio file from the translated text. category: Scenarios languages: @@ -128,7 +129,8 @@ cross_SQSMessageApp: cross_RDSDataTracker: title: Create an &AUR; Serverless work item tracker title_abbrev: Create an &AUR; Serverless work item tracker - synopsis: create a web application that tracks work items in an &AURlong; Serverless database and uses &SESlong; (&SES;) + synopsis: + create a web application that tracks work items in an &AURlong; Serverless database and uses &SESlong; (&SES;) to send reports. category: Scenarios languages: @@ -242,7 +244,8 @@ cross_DynamoDBDataTracker: cross_ApiGatewayDataTracker: title: Create an &ABP; REST API to track COVID-19 data title_abbrev: Create a REST API to track COVID-19 data - synopsis: create a REST API that simulates a system to track daily cases of COVID-19 in the United States, using fictional + synopsis: + create a REST API that simulates a system to track daily cases of COVID-19 in the United States, using fictional data. category: Scenarios languages: @@ -276,7 +279,8 @@ cross_ApiGatewayWebsocketChat: cross_AuroraRestLendingLibrary: title: Create a lending library REST API title_abbrev: Create a lending library REST API - synopsis: create a lending library where patrons can borrow and return books by using a REST API backed by an &AURlong; + synopsis: + create a lending library where patrons can borrow and return books by using a REST API backed by an &AURlong; database. category: Scenarios languages: @@ -318,8 +322,6 @@ cross_TextractExplorer: versions: - sdk_version: 3 block_content: cross_TextractExplorer_JavaScript_block.xml - add_services: - cognito-identity: Python: versions: - sdk_version: 3 @@ -327,6 +329,7 @@ cross_TextractExplorer: block_content: cross_TextractExplorer_Python_block.xml service_main: textract services: + cognito-identity: s3: sns: sqs: @@ -379,16 +382,10 @@ cross_LambdaAPIGateway: versions: - sdk_version: 2 block_content: cross_LambdaAPIGateway_Java_block.xml - add_services: - dynamodb: - sns: JavaScript: versions: - sdk_version: 3 block_content: cross_LambdaAPIGateway_JavaScript_block.xml - add_services: - dynamodb: - sns: Python: versions: - sdk_version: 3 @@ -397,7 +394,9 @@ cross_LambdaAPIGateway: service_main: lambda services: api-gateway: + dynamodb: lambda: + sns: cross_LambdaScheduledEvents: title: Use scheduled events to invoke a &LAM; function title_abbrev: Use scheduled events to invoke a &LAM; function @@ -408,27 +407,22 @@ cross_LambdaScheduledEvents: versions: - sdk_version: 2 block_content: cross_LambdaScheduledEvents_Java_block.xml - add_services: - dynamodb: - sns: JavaScript: versions: - sdk_version: 3 block_content: cross_LambdaScheduledEvents_JavaScript_block.xml - add_services: - dynamodb: - sns: Python: versions: - sdk_version: 3 github: python/example_code/lambda block_content: cross_LambdaScheduledEvents_Python_block.xml - add_services: - cloudwatch-logs: service_main: lambda services: + cloudwatch-logs: + dynamodb: eventbridge: lambda: + sns: cross_ServerlessWorkflows: title: Use &SFN; to invoke &LAM; functions title_abbrev: Use &SFN; to invoke &LAM; functions @@ -520,20 +514,18 @@ cross_RekognitionVideoDetection: versions: - sdk_version: 2 block_content: cross_RekognitionVideoAnalyzer_Java_block.xml - add_services: - s3: - ses: Python: versions: - sdk_version: 3 github: python/example_code/rekognition block_content: cross_RekognitionVideoDetection_Python_block.xml - add_services: - sns: - sqs: service_main: rekognition services: rekognition: + s3: + ses: + sns: + sqs: cross_DetectFaces: title: Detect faces in an image using an &AWS; SDK title_abbrev: Detect faces in an image @@ -608,7 +600,8 @@ cross_LambdaForBrowser: cross_ResilientService: title: Build and manage a resilient service using an &AWS; SDK title_abbrev: Build and manage a resilient service - synopsis: create a load-balanced web service that returns book, movie, and song recommendations. The example shows how the + synopsis: + create a load-balanced web service that returns book, movie, and song recommendations. The example shows how the service responds to failures, and how to restructure the service for more resilience when failures occur. synopsis_list: - Use an &ASlong; group to create &EC2long; (&EC2;) instances based on a launch template and to keep the number of instances @@ -699,12 +692,38 @@ cross_ResilientService: snippet_files: - javascriptv3/example_code/cross-services/wkflw-resilient-service/steps-destroy.js services: - auto-scaling: {CreateAutoScalingGroup, DescribeAutoScalingGroups, TerminateInstanceInAutoScalingGroup, AttachLoadBalancerTargetGroups, - DeleteAutoScalingGroup, UpdateAutoScalingGroup} - ec2: {DescribeIamInstanceProfileAssociations, ReplaceIamInstanceProfileAssociation, RebootInstances, CreateLaunchTemplate, - DeleteLaunchTemplate, DescribeAvailabilityZones, DescribeInstances, DescribeVpcs, DescribeSubnets} - elastic-load-balancing-v2: {DescribeLoadBalancers, CreateTargetGroup, DescribeTargetGroups, DeleteTargetGroup, CreateLoadBalancer, - CreateListener, DeleteLoadBalancer, DescribeTargetHealth} + auto-scaling: + { + CreateAutoScalingGroup, + DescribeAutoScalingGroups, + TerminateInstanceInAutoScalingGroup, + AttachLoadBalancerTargetGroups, + DeleteAutoScalingGroup, + UpdateAutoScalingGroup, + } + ec2: + { + DescribeIamInstanceProfileAssociations, + ReplaceIamInstanceProfileAssociation, + RebootInstances, + CreateLaunchTemplate, + DeleteLaunchTemplate, + DescribeAvailabilityZones, + DescribeInstances, + DescribeVpcs, + DescribeSubnets, + } + elastic-load-balancing-v2: + { + DescribeLoadBalancers, + CreateTargetGroup, + DescribeTargetGroups, + DeleteTargetGroup, + CreateLoadBalancer, + CreateListener, + DeleteLoadBalancer, + DescribeTargetHealth, + } iam: {CreateInstanceProfile, DeleteInstanceProfile} cross_FMPlayground: title: Create a sample application that offers playgrounds to interact with &BR; foundation models using an &AWS; SDK @@ -854,7 +873,8 @@ cross_CognitoAutoConfirmUser: snippet_files: - javascriptv3/example_code/cross-services/wkflw-pools-triggers/actions/dynamodb-actions.js services: - cognito-identity-provider: {UpdateUserPool, SignUp, InitiateAuth, DeleteUser} + cognito-identity-provider: + {UpdateUserPool, SignUp, InitiateAuth, DeleteUser} lambda: {} cross_CognitoAutoMigrateUser: title: Automatically migrate known &COG; users with a &LAM; function using an &AWS; SDK @@ -899,7 +919,15 @@ cross_CognitoAutoMigrateUser: snippet_tags: - gov2.cognito-identity-provider.Resources.complete services: - cognito-identity-provider: {UpdateUserPool, SignUp, InitiateAuth, ForgotPassword, ConfirmForgotPassword, DeleteUser} + cognito-identity-provider: + { + UpdateUserPool, + SignUp, + InitiateAuth, + ForgotPassword, + ConfirmForgotPassword, + DeleteUser, + } lambda: {} cross_CognitoCustomActivityLog: title: Write custom activity data with a &LAM; function after &COG; user authentication using an &AWS; SDK @@ -944,7 +972,14 @@ cross_CognitoCustomActivityLog: snippet_tags: - gov2.cognito-identity-provider.Resources.complete services: - cognito-identity-provider: {UpdateUserPool, InitiateAuth, DeleteUser, AdminCreateUser, AdminSetUserPassword} + cognito-identity-provider: + { + UpdateUserPool, + InitiateAuth, + DeleteUser, + AdminCreateUser, + AdminSetUserPassword, + } lambda: {} cross_MonitorDynamoDB: title: Monitor performance of &DDBlong; using an &AWS; SDK diff --git a/python/example_code/cloudwatch-logs/README.md b/python/example_code/cloudwatch-logs/README.md index a7da60c829c..63f8d0735c4 100644 --- a/python/example_code/cloudwatch-logs/README.md +++ b/python/example_code/cloudwatch-logs/README.md @@ -47,6 +47,7 @@ Code examples that show you how to accomplish a specific task by calling multipl functions within the same service. - [Run a large query](scenarios/large-query/exec.py) +- [Use scheduled events to invoke a Lambda function](../../example_code/lambda) @@ -80,6 +81,18 @@ python scenarios/large-query/exec.py +#### Use scheduled events to invoke a Lambda function + +This example shows you how to create an AWS Lambda function invoked by an Amazon EventBridge scheduled event. + + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/python/example_code/dynamodb/README.md b/python/example_code/dynamodb/README.md index 0853a0648a6..b3d81a5cda4 100644 --- a/python/example_code/dynamodb/README.md +++ b/python/example_code/dynamodb/README.md @@ -77,6 +77,8 @@ functions within the same service. - [Create a websocket chat application](../../cross_service/apigateway_websocket_chat) - [Query a table by using batches of PartiQL statements](partiql/scenario_partiql_batch.py) - [Query a table using PartiQL](partiql/scenario_partiql_single.py) +- [Use API Gateway to invoke a Lambda function](../../example_code/lambda) +- [Use scheduled events to invoke a Lambda function](../../example_code/lambda) @@ -259,6 +261,30 @@ python partiql/scenario_partiql_single.py +#### Use API Gateway to invoke a Lambda function + +This example shows you how to create an AWS Lambda function invoked by Amazon API Gateway. + + + + + + + + + +#### Use scheduled events to invoke a Lambda function + +This example shows you how to create an AWS Lambda function invoked by an Amazon EventBridge scheduled event. + + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. @@ -285,4 +311,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/s3/README.md b/python/example_code/s3/README.md index b5568cded93..1a80d184754 100644 --- a/python/example_code/s3/README.md +++ b/python/example_code/s3/README.md @@ -88,6 +88,7 @@ functions within the same service. - [Create an Amazon Textract explorer application](../../cross_service/textract_explorer) - [Detect entities in text extracted from an image](../../cross_service/textract_comprehend_notebook) - [Detect objects in images](../../cross_service/photo_analyzer) +- [Detect people and objects in a video](../../example_code/rekognition) - [Make conditional requests](scenarios/conditional_requests/scenario.py) - [Manage versioned objects in batches with a Lambda function](../../example_code/s3/s3_versioning) - [Upload or download large files](file_transfer/file_transfer.py) @@ -191,6 +192,18 @@ This example shows you how to build an app that uses Amazon Rekognition to detec +#### Detect people and objects in a video + +This example shows you how to detect people and objects in a video with Amazon Rekognition. + + + + + + + + + #### Make conditional requests This example shows you how to add preconditions to Amazon S3 requests. @@ -290,4 +303,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/ses/README.md b/python/example_code/ses/README.md index 8424785731b..557a434322b 100644 --- a/python/example_code/ses/README.md +++ b/python/example_code/ses/README.md @@ -68,6 +68,7 @@ functions within the same service. - [Create a web application to track DynamoDB data](../../cross_service/dynamodb_item_tracker) - [Create an Aurora Serverless work item tracker](../../cross_service/aurora_item_tracker) - [Detect objects in images](../../cross_service/photo_analyzer) +- [Detect people and objects in a video](../../example_code/rekognition) - [Generate credentials to connect to an SMTP endpoint](ses_generate_smtp_credentials.py) - [Verify an email identity and send messages](ses_email.py) @@ -139,6 +140,18 @@ This example shows you how to build an app that uses Amazon Rekognition to detec +#### Detect people and objects in a video + +This example shows you how to detect people and objects in a video with Amazon Rekognition. + + + + + + + + + #### Generate credentials to connect to an SMTP endpoint This example shows you how to generate credentials to connect to an Amazon SES SMTP endpoint. @@ -205,4 +218,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/sns/README.md b/python/example_code/sns/README.md index 69a60fc45e4..6946b1c7285 100644 --- a/python/example_code/sns/README.md +++ b/python/example_code/sns/README.md @@ -54,7 +54,10 @@ functions within the same service. - [Create an Amazon Textract explorer application](../../cross_service/textract_explorer) - [Create and publish to a FIFO topic](sns_fifo_topic.py) +- [Detect people and objects in a video](../../example_code/rekognition) - [Publish an SMS text message](sns_basics.py) +- [Use API Gateway to invoke a Lambda function](../../example_code/lambda) +- [Use scheduled events to invoke a Lambda function](../../example_code/lambda) @@ -105,6 +108,18 @@ python sns_fifo_topic.py +#### Detect people and objects in a video + +This example shows you how to detect people and objects in a video with Amazon Rekognition. + + + + + + + + + #### Publish an SMS text message This example shows you how to publish SMS messages using Amazon SNS. @@ -123,6 +138,30 @@ python sns_basics.py +#### Use API Gateway to invoke a Lambda function + +This example shows you how to create an AWS Lambda function invoked by Amazon API Gateway. + + + + + + + + + +#### Use scheduled events to invoke a Lambda function + +This example shows you how to create an AWS Lambda function invoked by an Amazon EventBridge scheduled event. + + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. @@ -149,4 +188,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/sqs/README.md b/python/example_code/sqs/README.md index 4bfc84d168e..a208669c236 100644 --- a/python/example_code/sqs/README.md +++ b/python/example_code/sqs/README.md @@ -56,6 +56,7 @@ functions within the same service. - [Create a messenger application](../../cross_service/stepfunctions_messenger) - [Create an Amazon Textract explorer application](../../cross_service/textract_explorer) - [Create and publish to a FIFO topic](../sns/sns_fifo_topic.py) +- [Detect people and objects in a video](../../example_code/rekognition) - [Send and receive batches of messages](message_wrapper.py) @@ -114,6 +115,18 @@ python ../sns/sns_fifo_topic.py +#### Detect people and objects in a video + +This example shows you how to detect people and objects in a video with Amazon Rekognition. + + + + + + + + + #### Send and receive batches of messages This example shows you how to do the following: @@ -162,4 +175,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 From 31a959c1df2ada0b770cf1f777af0773c5464836 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 10:42:05 -0500 Subject: [PATCH 069/144] updated logic --- .../entity/scenario/EntityResScenario.java | 72 ++++++++++--------- .../src/test/java/EntityResTests.java | 18 +++-- scenarios/basics/entity_resolution/README.md | 38 ++++++---- 3 files changed, 77 insertions(+), 51 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index a0551a628a9..1b17fd03f08 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -28,7 +28,6 @@ public class EntityResScenario { private static String workflowName = "workflow-"+ UUID.randomUUID(); public static void main(String[] args) throws InterruptedException { - String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); String jsonSchemaMappingArn = null; String csvSchemaMappingName = "csv-" + UUID.randomUUID(); @@ -126,13 +125,14 @@ Amazon Web Services (AWS) that helps organizations extract, link, and and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - In this example, the schema mapping lines up with the fields in the JSON objects. That is, + In this example, the schema mapping lines up with the fields in the JSON ans CSV objects. That is, it contains these fields: id, name, and email. """); waitForInputToContinue(scanner); try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); jsonSchemaMappingArn = response.schemaArn(); + logger.info("The JSON schema mapping ARN is "+jsonSchemaMappingArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -141,12 +141,11 @@ Amazon Web Services (AWS) that helps organizations extract, link, and try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); csvSchemaMappingArn = response.schemaArn(); + logger.info("The CSV schema mapping ARN is "+csvSchemaMappingArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } - - waitForInputToContinue(scanner); logger.info(DASHES); @@ -231,19 +230,19 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("8. Delete the AWS Entity Resolution Workflow."); + logger.info("8. View the results of the AWS Entity Resolution Workflow."); logger.info(""" - You cannot delete a workflow that is in a running state. - Would you like to wait for the workflow that we started in step 3 to complete. + You cannot view the result of the workflow that is in a running state. + In order to view the results, you need to wait for the workflow that we started in step 3 to complete. - If you choose not to wait, you will need to delete the workflow manually - in the AWS Management Console. + If you choose not to wait, you cannot view the results or delete the workflow. You would have to + perform both tasks manually in the AWS Management Console. This can take up to 30 mins (y/n). """); - String delAns = scanner.nextLine().trim(); - if (delAns.equalsIgnoreCase("y")) { - logger.info("You selected to delete Entity Resolution Workflow."); + String viewAns = scanner.nextLine().trim(); + if (viewAns.equalsIgnoreCase("y")) { + logger.info("You selected to view the Entity Resolution Workflow results."); waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); @@ -272,30 +271,38 @@ Amazon Web Services (AWS) that helps organizations extract, link, and the confidence level is lower for the differing email addresses. """); - try { - actions.deleteMatchingWorkflowAsync(workflowName).join(); - logger.info("Workflow deleted successfully!"); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); - } - } - waitForInputToContinue(scanner); - logger.info(DASHES); - logger.info(DASHES); - logger.info(""" + logger.info("Do you want to delete the resources, including workflow?"); + String delAns = scanner.nextLine().trim(); + if (delAns.equalsIgnoreCase("y")) { + try { + actions.deleteMatchingWorkflowAsync(workflowName).join(); + logger.info("Workflow deleted successfully!"); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + waitForInputToContinue(scanner); + logger.info(DASHES); + logger.info(""" Now we delete the CloudFormation stack, which deletes the resources that were created at the beginning """); + waitForInputToContinue(scanner); + logger.info(DASHES); + try { + deleteResources(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); + } + + } else { + logger.info("You can delete the Workflow later in the AWS Management console."); + } + } waitForInputToContinue(scanner); logger.info(DASHES); - try { - deleteResources(); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); - } logger.info(DASHES); logger.info("This concludes the AWS Entity Resolution scenario."); @@ -336,13 +343,13 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota // Check workflow status every 60 seconds if (secondsElapsed % 60 == 0 || remainingTime <= 0) { if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { - logger.info(""); // Move to the next line after countdown + logger.info(""); // Move to the next line after countdown. logger.info("Countdown complete: Workflow is in SUCCEEDED state!"); break; } } - // If countdown reaches zero, reset it for continuous countdown + // If countdown reaches zero, reset it for continuous countdown. if (remainingTime <= 0) { secondsElapsed = 0; } @@ -352,7 +359,6 @@ private static void deleteResources(){ CloudFormationHelper.emptyS3Bucket(glueBucketName); CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); logger.info("Resources deleted successfully!"); - } } // snippet-end:[entityres.java2_scenario.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index b1bb546d8b9..f3610d22dc9 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -75,11 +75,16 @@ public static void setUp() { } ] """; - if (!actions.doesObjectExist(dataS3bucket)) { - actions.uploadInputData(dataS3bucket, json); - } else { - System.out.println("The JSON exists in " + dataS3bucket); - } + + String csv = """ + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + """; + + actions.uploadInputData(dataS3bucket, json, csv); } @Test @@ -99,7 +104,8 @@ public void testCreateMapping() { @Order(2) public void testCreateMappingWorkflow() { assertDoesNotThrow(() -> { - workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + //workflowArn = actions.actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn + // , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); assertNotNull(workflowArn); }); logger.info("Test 2 passed"); diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 6e79288f02e..05a255335c4 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -1,27 +1,41 @@ +# AWS Entity Resolution Java Program + ## Overview -This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. The scenario covers various operations such as creating a schema mapping, creating a matching workflow, starting a matching job, and so on. +This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. This Java application demonstrates how to use AWS Entity Resolution to integrate and deduplicate data from multiple sources using machine learning-based matching. The program walks through setting up AWS resources, uploading structured data, defining schema mappings, creating a matching workflow, and running a matching job. -## Key Operations -1. **Create an AWS Entity Resolution schema mapping**: - - This step creates an AWS Entity Resolution schema mapping by invoking the `createSchemaMapping` method. +**Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. -2. **Create an AWS Entity Resolution workflow**: - - This step creates an AWS Entity Resolution matching workflow by invoking the `createMatchingWorkflow` method. +## Features -3. **Start a matching aorkflow**: - - This step starts the AWS Entity Resolution matching workflow by invoking the `startMatchingJob` method. +1. Uses AWS CloudFormation to create necessary resources: -4. **Get workflow job details**: - - This step gets workflow job details by invoking the `getMatchingJob` method. +- AWS Glue Data Catalog table +- AWS IAM role -**Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. +- AWS S3 bucket + +- AWS Entity Resolution Schema + +2. Uploads sample JSON and CSV data to S3 + +3. Creates schema mappings for JSON and CSV datasets + +4. Creates and starts an Entity Resolution matching workflow + +5. Retrieves job details and schema mappings + +6. Lists available schema mappings + +7. Tags AWS resources for better organization + +8. Views the results of the workflow ## Resources This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, -an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. +an AWS Glue database, and an S3 bucket. A CDK script is provided to create these resources. See the resources [Readme](../../../resources/cdk/entityresolution_resources/README.md) file. ## Implementations From 84dd357543c8d4f2d04f98501692b0d6954fdfcf Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 10:43:52 -0500 Subject: [PATCH 070/144] updated readme --- scenarios/basics/entity_resolution/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 05a255335c4..14679cbcc52 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -1,7 +1,7 @@ -# AWS Entity Resolution Java Program +# AWS Entity Resolution Program ## Overview -This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. This Java application demonstrates how to use AWS Entity Resolution to integrate and deduplicate data from multiple sources using machine learning-based matching. The program walks through setting up AWS resources, uploading structured data, defining schema mappings, creating a matching workflow, and running a matching job. +This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. This application demonstrates how to use AWS Entity Resolution to integrate and deduplicate data from multiple sources using machine learning-based matching. The program walks through setting up AWS resources, uploading structured data, defining schema mappings, creating a matching workflow, and running a matching job. **Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. From f36435d464e0390406b3f08ef09724ac865e6b8d Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Tue, 25 Feb 2025 12:57:24 -0500 Subject: [PATCH 071/144] Swift: Add MVP set of action examples for SQS (#7256) * Swift: Add MVP set of action examples for SQS This commit adds action examples for SQS in Swift. --- .doc_gen/metadata/sqs_metadata.yaml | 75 +++++++++++ .../sqs/CreateQueue/Package.swift | 40 ++++++ .../sqs/CreateQueue/Sources/entry.swift | 61 +++++++++ .../sqs/DeleteMessageBatch/Package.swift | 40 ++++++ .../DeleteMessageBatch/Sources/entry.swift | 116 ++++++++++++++++++ .../sqs/DeleteQueue/Package.swift | 40 ++++++ .../sqs/DeleteQueue/Sources/entry.swift | 61 +++++++++ .../sqs/GetQueueAttributes/Package.swift | 40 ++++++ .../GetQueueAttributes/Sources/entry.swift | 75 +++++++++++ swift/example_code/sqs/README.md | 104 ++++++++++++++++ .../sqs/ReceiveMessage/Package.swift | 40 ++++++ .../sqs/ReceiveMessage/Sources/entry.swift | 71 +++++++++++ .../sqs/SetQueueAttributes/Package.swift | 40 ++++++ .../SetQueueAttributes/Sources/entry.swift | 65 ++++++++++ swift/example_code/sqs/basics/Package.swift | 42 +++++++ .../sqs/basics/Sources/entry.swift | 75 +++++++++++ 16 files changed, 985 insertions(+) create mode 100644 swift/example_code/sqs/CreateQueue/Package.swift create mode 100644 swift/example_code/sqs/CreateQueue/Sources/entry.swift create mode 100644 swift/example_code/sqs/DeleteMessageBatch/Package.swift create mode 100644 swift/example_code/sqs/DeleteMessageBatch/Sources/entry.swift create mode 100644 swift/example_code/sqs/DeleteQueue/Package.swift create mode 100644 swift/example_code/sqs/DeleteQueue/Sources/entry.swift create mode 100644 swift/example_code/sqs/GetQueueAttributes/Package.swift create mode 100644 swift/example_code/sqs/GetQueueAttributes/Sources/entry.swift create mode 100644 swift/example_code/sqs/README.md create mode 100644 swift/example_code/sqs/ReceiveMessage/Package.swift create mode 100644 swift/example_code/sqs/ReceiveMessage/Sources/entry.swift create mode 100644 swift/example_code/sqs/SetQueueAttributes/Package.swift create mode 100644 swift/example_code/sqs/SetQueueAttributes/Sources/entry.swift create mode 100644 swift/example_code/sqs/basics/Package.swift create mode 100644 swift/example_code/sqs/basics/Sources/entry.swift diff --git a/.doc_gen/metadata/sqs_metadata.yaml b/.doc_gen/metadata/sqs_metadata.yaml index 166119d1930..f3553187992 100644 --- a/.doc_gen/metadata/sqs_metadata.yaml +++ b/.doc_gen/metadata/sqs_metadata.yaml @@ -58,6 +58,18 @@ sqs_Hello: - description: Initialize an &SQS; client and list queues. snippet_tags: - javascript.v3.sqs.hello + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: The Package.swift file. + snippet_tags: + - swift.sqs.basics.package + - description: The Swift source code, entry.swift. + snippet_tags: + - swift.sqs.basics services: sqs: {ListQueues} sqs_CreateQueue: @@ -163,6 +175,15 @@ sqs_CreateQueue: snippet_tags: - cpp.example_code.sqs.CreateQueue.config - cpp.example_code.sqs.CreateQueue + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.CreateQueue services: sqs: {CreateQueue} sqs_GetQueueUrl: @@ -320,6 +341,15 @@ sqs_ListQueues: snippet_tags: - cpp.example_code.sqs.ListQueues.config - cpp.example_code.sqs.ListQueues + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.ListQueues services: sqs: {ListQueues} sqs_DeleteQueue: @@ -414,6 +444,15 @@ sqs_DeleteQueue: snippet_tags: - cpp.example_code.sqs.DeleteQueue.config - cpp.example_code.sqs.DeleteQueue + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.DeleteQueue services: sqs: {DeleteQueue} sqs_SendMessage: @@ -649,6 +688,15 @@ sqs_ReceiveMessage: snippet_tags: - cpp.example_code.sqs.ReceiveMessage.config - cpp.example_code.sqs.ReceiveMessage + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.ReceiveMessage services: sqs: {ReceiveMessage} sqs_DeleteMessage: @@ -765,6 +813,15 @@ sqs_DeleteMessageBatch: - description: snippet_tags: - sqs.JavaScript.messages.receiveMessageV3 + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.DeleteMessageBatch services: sqs: {DeleteMessageBatch} sqs_Scenario_SendReceiveBatch: @@ -836,6 +893,15 @@ sqs_GetQueueAttributes: - description: snippet_tags: - javascript.v3.sqs.actions.GetQueueAttributes + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.GetQueueAttributes services: sqs: {GetQueueAttributes} sqs_ChangeMessageVisibility: @@ -931,6 +997,15 @@ sqs_SetQueueAttributes: - description: Configure a dead-letter queue. snippet_tags: - sqs.JavaScript.deadLetter.setQueueAttributesV3 + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.SetQueueAttributes services: sqs: {SetQueueAttributes} sqs_Scenario_TopicsAndQueues: diff --git a/swift/example_code/sqs/CreateQueue/Package.swift b/swift/example_code/sqs/CreateQueue/Package.swift new file mode 100644 index 00000000000..bf475e99d8b --- /dev/null +++ b/swift/example_code/sqs/CreateQueue/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "createqueue", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "createqueue", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/CreateQueue/Sources/entry.swift b/swift/example_code/sqs/CreateQueue/Sources/entry.swift new file mode 100644 index 00000000000..afb2b83fe48 --- /dev/null +++ b/swift/example_code/sqs/CreateQueue/Sources/entry.swift @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Queue +// Service client to create an available Amazon SQS queue. + +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The name of the Amazon SQS queue to create") + var queueName: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "createqueue", + abstract: """ + This example shows how to create a new Amazon SQS queue. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.CreateQueue] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + let output = try await sqsClient.createQueue( + input: CreateQueueInput( + queueName: queueName + ) + ) + + guard let queueUrl = output.queueUrl else { + print("No queue URL returned.") + return + } + // snippet-end:[swift.sqs.CreateQueue] + print("Created queue named \(queueName) with URL \(queueUrl).") + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sqs/DeleteMessageBatch/Package.swift b/swift/example_code/sqs/DeleteMessageBatch/Package.swift new file mode 100644 index 00000000000..13a005cf19f --- /dev/null +++ b/swift/example_code/sqs/DeleteMessageBatch/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "deletemessages", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "deletemessages", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/DeleteMessageBatch/Sources/entry.swift b/swift/example_code/sqs/DeleteMessageBatch/Sources/entry.swift new file mode 100644 index 00000000000..d76157fe98b --- /dev/null +++ b/swift/example_code/sqs/DeleteMessageBatch/Sources/entry.swift @@ -0,0 +1,116 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Queue +// Service client to delete messages from an Amazon SQS queue. + +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "The URL of the Amazon SQS queue from which to delete messages") + var queue: String + @Argument(help: "Receipt handle(s) of the message(s) to delete") + var handles: [String] + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "deletemessages", + abstract: """ + This example shows how to delete a batch of messages from an Amazon SQS queue. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.DeleteMessageBatch] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + // Create the list of message entries. + + var entries: [SQSClientTypes.DeleteMessageBatchRequestEntry] = [] + var messageNumber = 1 + + for handle in handles { + let entry = SQSClientTypes.DeleteMessageBatchRequestEntry( + id: "\(messageNumber)", + receiptHandle: handle + ) + entries.append(entry) + messageNumber += 1 + } + + // Delete the messages. + + let output = try await sqsClient.deleteMessageBatch( + input: DeleteMessageBatchInput( + entries: entries, + queueUrl: queue + ) + ) + + // Get the lists of failed and successful deletions from the output. + + guard let failedEntries = output.failed else { + print("Failed deletion list is missing!") + return + } + guard let successfulEntries = output.successful else { + print("Successful deletion list is missing!") + return + } + + // Display a list of the failed deletions along with their + // corresponding explanation messages. + + if failedEntries.count != 0 { + print("Failed deletions:") + + for entry in failedEntries { + print("Message #\(entry.id ?? "") failed: \(entry.message ?? "")") + } + } else { + print("No failed deletions.") + } + + // Output a list of the message numbers that were successfully deleted. + + if successfulEntries.count != 0 { + var successes = "" + + for entry in successfulEntries { + if successes.count == 0 { + successes = entry.id ?? "" + } else { + successes = "\(successes), \(entry.id ?? "")" + } + } + print("Succeeded: ", successes) + } else { + print("No successful deletions.") + } + + // snippet-end:[swift.sqs.DeleteMessageBatch] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sqs/DeleteQueue/Package.swift b/swift/example_code/sqs/DeleteQueue/Package.swift new file mode 100644 index 00000000000..37fc04c96dc --- /dev/null +++ b/swift/example_code/sqs/DeleteQueue/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "deletequeue", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "deletequeue", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/DeleteQueue/Sources/entry.swift b/swift/example_code/sqs/DeleteQueue/Sources/entry.swift new file mode 100644 index 00000000000..4bb0a546e39 --- /dev/null +++ b/swift/example_code/sqs/DeleteQueue/Sources/entry.swift @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to delete an Amazon SQS queue. + +// snippet-start:[swift.sqs.basics] +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The URL of the Amazon SQS queue to delete") + var queueUrl: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "deletequeue", + abstract: """ + This example shows how to delete an Amazon SQS queue. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.DeleteQueue] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + do { + _ = try await sqsClient.deleteQueue( + input: DeleteQueueInput( + queueUrl: queueUrl + ) + ) + } catch _ as AWSSQS.QueueDoesNotExist { + print("Error: The specified queue doesn't exist.") + return + } + // snippet-end:[swift.sqs.DeleteQueue] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} +// snippet-end:[swift.sqs.basics] diff --git a/swift/example_code/sqs/GetQueueAttributes/Package.swift b/swift/example_code/sqs/GetQueueAttributes/Package.swift new file mode 100644 index 00000000000..a46032ef803 --- /dev/null +++ b/swift/example_code/sqs/GetQueueAttributes/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "getqueueattributes", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "getqueueattributes", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/GetQueueAttributes/Sources/entry.swift b/swift/example_code/sqs/GetQueueAttributes/Sources/entry.swift new file mode 100644 index 00000000000..1255f3c7eb0 --- /dev/null +++ b/swift/example_code/sqs/GetQueueAttributes/Sources/entry.swift @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Queue +// Service client to get the attributes of an available Amazon SQS queue. + +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The URL of the Amazon SQS queue to get the attributes of") + var url: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "getqueueattributes", + abstract: """ + This example shows how to get an Amazon SQS queue's attributes. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.GetQueueAttributes] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + let output = try await sqsClient.getQueueAttributes( + input: GetQueueAttributesInput( + attributeNames: [ + .approximatenumberofmessages, + .maximummessagesize + ], + queueUrl: url + ) + ) + + guard let attributes = output.attributes else { + print("No queue attributes returned.") + return + } + + for (attr, value) in attributes { + switch(attr) { + case "ApproximateNumberOfMessages": + print("Approximate message count: \(value)") + case "MaximumMessageSize": + print("Maximum message size: \(value)kB") + default: + continue + } + } + // snippet-end:[swift.sqs.GetQueueAttributes] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sqs/README.md b/swift/example_code/sqs/README.md new file mode 100644 index 00000000000..f9a0e79e12b --- /dev/null +++ b/swift/example_code/sqs/README.md @@ -0,0 +1,104 @@ +# Amazon SQS code examples for the SDK for Swift + +## Overview + +Shows how to use the AWS SDK for Swift to work with Amazon Simple Queue Service (Amazon SQS). + + + + +_Amazon SQS is a fully managed message queuing service that makes it easy to decouple and scale microservices, distributed systems, and serverless applications._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `swift` folder. + + + + + +### Get started + +- [Hello Amazon SQS](basics/Package.swift#L8) (`ListQueues`) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreateQueue](CreateQueue/Sources/entry.swift#L29) +- [DeleteMessageBatch](DeleteMessageBatch/Sources/entry.swift#L31) +- [DeleteQueue](DeleteQueue/Sources/entry.swift#L29) +- [GetQueueAttributes](GetQueueAttributes/Sources/entry.swift#L29) +- [ListQueues](basics/Sources/entry.swift#L28) +- [ReceiveMessage](ReceiveMessage/Sources/entry.swift#L31) +- [SetQueueAttributes](SetQueueAttributes/Sources/entry.swift#L32) + + + + + +## Run the examples + +### Instructions + +To build any of these examples from a terminal window, navigate into its +directory, then use the following command: + +``` +$ swift build +``` + +To build one of these examples in Xcode, navigate to the example's directory +(such as the `ListUsers` directory, to build that example). Then type `xed.` +to open the example directory in Xcode. You can then use standard Xcode build +and run commands. + + + + +#### Hello Amazon SQS + +This example shows you how to get started using Amazon SQS. + + + +### 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 `swift` folder. + + + + + + +## Additional resources + +- [Amazon SQS Developer Guide](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html) +- [Amazon SQS API Reference](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/Welcome.html) +- [SDK for Swift Amazon SQS reference](https://sdk.amazonaws.com/swift/api/awssqs/latest/documentation/awssqs) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/swift/example_code/sqs/ReceiveMessage/Package.swift b/swift/example_code/sqs/ReceiveMessage/Package.swift new file mode 100644 index 00000000000..12fac47b32e --- /dev/null +++ b/swift/example_code/sqs/ReceiveMessage/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "receivemessage", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "receivemessage", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/ReceiveMessage/Sources/entry.swift b/swift/example_code/sqs/ReceiveMessage/Sources/entry.swift new file mode 100644 index 00000000000..73433736dcf --- /dev/null +++ b/swift/example_code/sqs/ReceiveMessage/Sources/entry.swift @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Queue +// Service client to get the attributes of an available Amazon SQS queue. + +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "The maximum number of messages to receive") + var maxMessages = 10 + @Argument(help: "The URL of the Amazon SQS queue to get the attributes of") + var url: String + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "receivemessage", + abstract: """ + This example shows how to receive messages from an Amazon SQS queue. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.ReceiveMessage] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + let output = try await sqsClient.receiveMessage( + input: ReceiveMessageInput( + maxNumberOfMessages: maxMessages, + queueUrl: url + ) + ) + + guard let messages = output.messages else { + print("No messages received.") + return + } + + for message in messages { + print("Message ID: \(message.messageId ?? "")") + print("Receipt handle: \(message.receiptHandle ?? "")") + print(message.body ?? "") + print("---") + } + + // snippet-end:[swift.sqs.ReceiveMessage] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sqs/SetQueueAttributes/Package.swift b/swift/example_code/sqs/SetQueueAttributes/Package.swift new file mode 100644 index 00000000000..991a4a13f4b --- /dev/null +++ b/swift/example_code/sqs/SetQueueAttributes/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "configqueue", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "configqueue", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/SetQueueAttributes/Sources/entry.swift b/swift/example_code/sqs/SetQueueAttributes/Sources/entry.swift new file mode 100644 index 00000000000..5477f331dba --- /dev/null +++ b/swift/example_code/sqs/SetQueueAttributes/Sources/entry.swift @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Queue +// Service client to get the attributes of an available Amazon SQS queue. + +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Argument(help: "The URL of the Amazon SQS queue to set attributes of") + var url: String + @Option(help: "Maximum size of a message in bytes, from 1024 to 262144") + var maxSize: Int + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "configqueue", + abstract: """ + This example shows how to set attributes of an Amazon + SQS queue, using the SQS client's setQueueAttributes() function. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.SetQueueAttributes] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + do { + _ = try await sqsClient.setQueueAttributes( + input: SetQueueAttributesInput( + attributes: [ + "MaximumMessageSize": "\(maxSize)" + ], + queueUrl: url + ) + ) + } catch _ as AWSSQS.InvalidAttributeValue { + print("Invalid maximum message size: \(maxSize) kB.") + } + // snippet-end:[swift.sqs.SetQueueAttributes] + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sqs/basics/Package.swift b/swift/example_code/sqs/basics/Package.swift new file mode 100644 index 00000000000..c8cab043cef --- /dev/null +++ b/swift/example_code/sqs/basics/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +// snippet-start:[swift.sqs.basics.package] +import PackageDescription + +let package = Package( + name: "sqs-basics", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "sqs-basics", + dependencies: [ + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) +// snippet-end:[swift.sqs.basics.package] diff --git a/swift/example_code/sqs/basics/Sources/entry.swift b/swift/example_code/sqs/basics/Sources/entry.swift new file mode 100644 index 00000000000..39fa21499ab --- /dev/null +++ b/swift/example_code/sqs/basics/Sources/entry.swift @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// An example demonstrating how to set up and use an Amazon Simple Notification +// Service client to list your available Amazon SQS queues. + +// snippet-start:[swift.sqs.basics] +import ArgumentParser +import AWSClientRuntime +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "sqs-basics", + abstract: """ + This example shows how to list all of your available Amazon SQS queues. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + // snippet-start:[swift.sqs.ListQueues] + let config = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: config) + + var queues: [String] = [] + let outputPages = sqsClient.listQueuesPaginated( + input: ListQueuesInput() + ) + + // Each time a page of results arrives, process its contents. + + for try await output in outputPages { + guard let urls = output.queueUrls else { + print("No queues found.") + return + } + + // Iterate over the queue URLs listed on this page, adding them + // to the `queues` array. + + for queueUrl in urls { + queues.append(queueUrl) + } + } + // snippet-end:[swift.sqs.ListQueues] + + print("You have \(queues.count) queues:") + for queue in queues { + print(" \(queue)") + } + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} +// snippet-end:[swift.sqs.basics] From 1b6b002ee33c846cf756200cd49711d5ddfbfafe Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 14:36:57 -0500 Subject: [PATCH 072/144] updated YAML file --- .../metadata/entityresolution_metadata.yaml | 12 ++ .../entity/scenario/EntityResActions.java | 27 +++- .../entity/scenario/EntityResScenario.java | 134 ++++++++++-------- .../src/test/java/EntityResTests.java | 128 +++++++---------- .../basics/entity_resolution/SPECIFICATION.md | 1 + 5 files changed, 156 insertions(+), 146 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index c9c23f58645..c25218b5164 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -14,6 +14,18 @@ entityresolution_Hello: - entityres.java2_hello.main services: entityresolution: {listMatchingWorkflows} +entityresolution_DeleteSchemaMapping: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_delete_mappings.main + services: + entityresolution: {entityresolution_DeleteSchemaMapping} entityresolution_TagEntityResource: languages: Java: diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 7876abfcf46..5c6e24c6016 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -17,8 +17,8 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.DeleteSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; -import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; @@ -41,7 +41,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { @@ -113,8 +112,28 @@ public static S3AsyncClient getS3AsyncClient() { return s3AsyncClient; } - // snippet-start:[entityres.java2_list_mappings.main] + // snippet-start:[entityres.java2_delete_mappings.main] + /** + * Deletes the schema mapping asynchronously. + * + * @param schemaName the name of the schema to delete + * @return a {@link CompletableFuture} that completes when the schema mapping is deleted successfully, + * or throws a {@link RuntimeException} if the deletion fails + */ + public CompletableFuture deleteSchemaMappingAsync(String schemaName) { + DeleteSchemaMappingRequest request = DeleteSchemaMappingRequest.builder() + .schemaName(schemaName) + .build(); + + return getResolutionAsyncClient().deleteSchemaMapping(request) + .thenRun(() -> logger.info("Schema mapping '{}' deleted successfully.", schemaName)) + .exceptionally(ex -> { + throw new RuntimeException("Failed to delete schema mapping: " + schemaName, ex); + }); + } + // snippet-end:[entityres.java2_delete_mappings.main] + // snippet-start:[entityres.java2_list_mappings.main] /** * Lists the schema mappings associated with the current AWS account. This method uses an asynchronous paginator to * retrieve the schema mappings, and prints the name of each schema mapping to the console. @@ -162,7 +181,6 @@ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) // snippet-end:[entityres.java2_delete_matching_workflow.main] // snippet-start:[entityres.java2_create_schema.main] - /** * Creates a schema mapping asynchronously. * @@ -203,7 +221,6 @@ public CompletableFuture createSchemaMappingAsync(S // snippet-end:[entityres.java2_create_schema.main] // snippet-start:[entityres.java2_get_schema_mapping.main] - /** * Retrieves the schema mapping asynchronously. * diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 1b17fd03f08..9899c69a34e 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.JobMetrics; import java.util.Map; @@ -25,13 +26,12 @@ public class EntityResScenario { private static final String JSON_GLUE_TABLE_ARN_KEY = "JsonErGlueTableArn"; private static final String CSV_GLUE_TABLE_ARN_KEY = "CsvErGlueTableArn"; private static String glueBucketName; - private static String workflowName = "workflow-"+ UUID.randomUUID(); + private static String workflowName = "workflow-" + UUID.randomUUID(); public static void main(String[] args) throws InterruptedException { String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); String jsonSchemaMappingArn = null; String csvSchemaMappingName = "csv-" + UUID.randomUUID(); - String csvSchemaMappingArn = null; String roleARN; String csvGlueTableArn; String jsonGlueTableArn; @@ -69,15 +69,15 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(""" - To prepare the AWS resources needed for this scenario application, the next step uploads - a CloudFormation template whose resulting stack creates the following resources: - - An AWS Glue Data Catalog table - - An AWS IAM role - - An AWS S3 bucket - - An AWS Entity Resolution Schema - - It can take a couple minutes for the Stack to finish creating the resources. - """); + To prepare the AWS resources needed for this scenario application, the next step uploads + a CloudFormation template whose resulting stack creates the following resources: + - An AWS Glue Data Catalog table + - An AWS IAM role + - An AWS S3 bucket + - An AWS Entity Resolution Schema + + It can take a couple minutes for the Stack to finish creating the resources. + """); waitForInputToContinue(scanner); logger.info("Generating resources..."); CloudFormationHelper.deployCloudFormationStack(STACK_NAME); @@ -95,19 +95,19 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Entity Resolution service. */ String json = """ - {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} - {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} - {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} - """; + {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} + {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} + {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} + """; logger.info("Upload the following JSON objects to the {} S3 bucket.", glueBucketName); logger.info(json); String csv = """ - id,name,email,phone - 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 - 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 - 3,Charlie Black,charlie.black@company.com,345-567-1234 - 7,Jane E. Doe,jane_doe@company.com,111-222-3333 - """; + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + """; logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); logger.info(csv); waitForInputToContinue(scanner); @@ -131,8 +131,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); - jsonSchemaMappingArn = response.schemaArn(); - logger.info("The JSON schema mapping ARN is "+jsonSchemaMappingArn); + jsonSchemaMappingName = response.schemaName(); + logger.info("The JSON schema mapping name is " + jsonSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -140,8 +140,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); - csvSchemaMappingArn = response.schemaArn(); - logger.info("The CSV schema mapping ARN is "+csvSchemaMappingArn); + csvSchemaMappingName = response.schemaName(); + logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -159,13 +159,12 @@ Amazon Web Services (AWS) that helps organizations extract, link, and it evaluates attributes like names or emails to detect duplicates or relationships, even with variations or inconsistencies. The workflow outputs consolidated, de-duplicated data. - + We will use the machine learning-based matching technique. """); waitForInputToContinue(scanner); try { - String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn - , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn, jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); @@ -188,7 +187,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("4. While the matching job is running, let's look at other API methods. First, let's get details for job "+jobId); + logger.info("4. While the matching job is running, let's look at other API methods. First, let's get details for job " + jobId); waitForInputToContinue(scanner); try { actions.getMatchingJobAsync(jobId, workflowName).join(); @@ -202,8 +201,9 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("5. Get the schema mapping for the JSON data."); waitForInputToContinue(scanner); try { - actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); - logger.info("Schema mapping retrieval completed."); + GetSchemaMappingResponse response = actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); + jsonSchemaMappingArn = response.schemaArn(); + logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); } @@ -234,16 +234,15 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(""" You cannot view the result of the workflow that is in a running state. In order to view the results, you need to wait for the workflow that we started in step 3 to complete. - + If you choose not to wait, you cannot view the results or delete the workflow. You would have to perform both tasks manually in the AWS Management Console. - + This can take up to 30 mins (y/n). """); String viewAns = scanner.nextLine().trim(); if (viewAns.equalsIgnoreCase("y")) { logger.info("You selected to view the Entity Resolution Workflow results."); - waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); logger.info("Number of input records: {}", metrics.inputRecords()); @@ -251,28 +250,28 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); logger.info(""" - - The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: - - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - - Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; - For example 'Bob Smith Jr.' compared to 'Bob Smith'. - The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, - the confidence level is lower for the differing email addresses. - - """); + + The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + + Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; + For example 'Bob Smith Jr.' compared to 'Bob Smith'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, + the confidence level is lower for the differing email addresses. + + """); - logger.info("Do you want to delete the resources, including workflow?"); + logger.info("Do you want to delete the resources, including the workflow?"); String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { try { @@ -282,12 +281,22 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Throwable cause = ce.getCause(); logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } + + try { + // Delete both schema mappings. + actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); + actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); + logger.info("Both schema mappings were deleted successfully!"); + } catch (RuntimeException e) { + logger.error("Error deleting schema mapping: {}", e.getMessage()); + } + waitForInputToContinue(scanner); logger.info(DASHES); logger.info(""" - Now we delete the CloudFormation stack, which deletes - the resources that were created at the beginning - """); + Now we delete the CloudFormation stack, which deletes + the resources that were created at the beginning + """); waitForInputToContinue(scanner); logger.info(DASHES); try { @@ -330,17 +339,17 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota int secondsElapsed = 0; while (true) { - // Calculate display minutes and seconds + // Calculate display minutes and seconds. int remainingTime = totalSeconds - secondsElapsed; int displayMinutes = remainingTime / 60; int displaySeconds = remainingTime % 60; - // Print the countdown + // Print the countdown. System.out.printf("\r%02d:%02d", displayMinutes, displaySeconds); Thread.sleep(1000); // Wait for 1 second secondsElapsed++; - // Check workflow status every 60 seconds + // Check workflow status every 60 seconds. if (secondsElapsed % 60 == 0 || remainingTime <= 0) { if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { logger.info(""); // Move to the next line after countdown. @@ -355,7 +364,8 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } } - private static void deleteResources(){ + + private static void deleteResources() { CloudFormationHelper.emptyS3Bucket(glueBucketName); CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); logger.info("Resources deleted successfully!"); diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index f3610d22dc9..03f2c75980d 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -2,23 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 +import com.example.entity.scenario.CloudFormationHelper; import com.example.entity.scenario.EntityResActions; -import com.google.gson.Gson; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; -import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; -import java.util.Random; + +import java.util.Map; +import java.util.UUID; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; import org.slf4j.Logger; @@ -28,33 +25,40 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class EntityResTests { private static final Logger logger = LoggerFactory.getLogger(EntityResTests.class); - private static String workflowName = ""; - private static String schemaName = ""; private static String roleARN = ""; - private static String dataS3bucket = ""; - private static String outputBucket = ""; - private static String inputGlueTableArn = ""; - private static String mappingARN = ""; + private static String csvMappingARN = ""; + private static String jsonMappingARN = ""; private static String jobId = ""; - private static String workflowArn =""; - private static EntityResActions actions = new EntityResActions(); + private static String glueBucketName = ""; + + private static String csvGlueTableArn = ""; + + private static String jsonGlueTableArn = ""; + + private static final String STACK_NAME = "EntityResolutionCdkStack"; + + private static final String ENTITY_RESOLUTION_ROLE_ARN_KEY = "EntityResolutionRoleArn"; + private static final String GLUE_DATA_BUCKET_NAME_KEY = "GlueDataBucketName"; + private static final String JSON_GLUE_TABLE_ARN_KEY = "JsonErGlueTableArn"; + private static final String CSV_GLUE_TABLE_ARN_KEY = "CsvErGlueTableArn"; + + private static String workflowArn = ""; + private static final String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); + private static final String csvSchemaMappingName = "csv-" + UUID.randomUUID(); + private static final String workflowName = "workflow-"+ UUID.randomUUID(); + private static final EntityResActions actions = new EntityResActions(); @BeforeAll public static void setUp() { - Random random = new Random(); - int randomValue = random.nextInt(10000) + 1; - workflowName = "MyMatchingWorkflow"+randomValue; - schemaName = "schema"+randomValue; - Gson gson = new Gson(); - String jsonVal = getSecretValues(); - SecretValues values = gson.fromJson(jsonVal, SecretValues.class); - roleARN = values.getRoleARN(); - dataS3bucket = values.getDataS3bucket(); - outputBucket = values.getOutputBucket(); - inputGlueTableArn = values.getInputGlueTableArn(); + CloudFormationHelper.deployCloudFormationStack(STACK_NAME); + Map outputsMap = CloudFormationHelper.getStackOutputsAsync(STACK_NAME).join(); + roleARN = outputsMap.get(ENTITY_RESOLUTION_ROLE_ARN_KEY); + glueBucketName = outputsMap.get(GLUE_DATA_BUCKET_NAME_KEY); + csvGlueTableArn = outputsMap.get(CSV_GLUE_TABLE_ARN_KEY); + jsonGlueTableArn = outputsMap.get(JSON_GLUE_TABLE_ARN_KEY); String json = """ [ @@ -84,7 +88,7 @@ public static void setUp() { 7,Jane E. Doe,jane_doe@company.com,111-222-3333 """; - actions.uploadInputData(dataS3bucket, json, csv); + actions.uploadInputData(glueBucketName, json, csv); } @Test @@ -92,9 +96,15 @@ public static void setUp() { @Order(1) public void testCreateMapping() { assertDoesNotThrow(() -> { - CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); - mappingARN = response.schemaArn(); - assertNotNull(mappingARN); + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); + jsonMappingARN = response.schemaArn(); + assertNotNull(jsonMappingARN); + }); + + assertDoesNotThrow(() -> { + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); + csvMappingARN = response.schemaArn(); + assertNotNull(csvMappingARN); }); logger.info("Test 1 passed"); } @@ -104,8 +114,7 @@ public void testCreateMapping() { @Order(2) public void testCreateMappingWorkflow() { assertDoesNotThrow(() -> { - //workflowArn = actions.actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn - // , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn, jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); assertNotNull(workflowArn); }); logger.info("Test 2 passed"); @@ -137,7 +146,7 @@ public void testGetJobDetails() { @Order(5) public void testtSchemaMappingDetails() { assertDoesNotThrow(() -> { - actions.getSchemaMappingAsync(schemaName).join(); + actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); }); logger.info("Test 5 passed"); } @@ -146,9 +155,7 @@ public void testtSchemaMappingDetails() { @Tag("IntegrationTest") @Order(6) public void testListSchemaMappings() { - assertDoesNotThrow(() -> { - actions.ListSchemaMappings(); - }); + assertDoesNotThrow(actions::ListSchemaMappings); logger.info("Test 6 passed"); } @@ -157,7 +164,7 @@ public void testListSchemaMappings() { @Order(7) public void testLTagResources() { assertDoesNotThrow(() -> { - actions.tagEntityResource(mappingARN).join(); + actions.tagEntityResource(csvMappingARN).join(); }); logger.info("Test 7 passed"); } @@ -170,48 +177,11 @@ public void testLDeleteMapping() { logger.info("Wait 30 mins for the workflow to complete"); Thread.sleep(1800000); actions.deleteMatchingWorkflowAsync(workflowName).join(); + actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); + actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); + CloudFormationHelper.emptyS3Bucket(glueBucketName); + CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); }); logger.info("Test 8 passed"); } - - private static String getSecretValues() { - SecretsManagerClient secretClient = SecretsManagerClient.builder() - .region(Region.US_EAST_1) - .build(); - String secretName = "test/entity"; - - GetSecretValueRequest valueRequest = GetSecretValueRequest.builder() - .secretId(secretName) - .build(); - - GetSecretValueResponse valueResponse = secretClient.getSecretValue(valueRequest); - return valueResponse.secretString(); - } - - @Nested - @DisplayName("A class used to get test values from test/cognito (an AWS Secrets Manager secret)") - class SecretValues { - private String roleARN; - private String dataS3bucket; - - private String outputBucket; - - private String inputGlueTableArn; - - public String getRoleARN() { - return roleARN; - } - - public String getDataS3bucket() { - return dataS3bucket; - } - - public String getOutputBucket() { - return outputBucket; - } - - public String getInputGlueTableArn() { - return inputGlueTableArn; - } - } } diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index f7c4c616544..22bb26e21de 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -281,6 +281,7 @@ The following table describes the metadata used in this Basics Scenario. | `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | | `tagResource ` | entity_metadata.yaml | entity_TagResource | | `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | +| `deleteMapping ` | entity_metadata.yaml | entity_DeleteSchemaMapping | | `listMappingJobs ` | entity_metadata.yaml | entity_Hello | | `scenario` | entity_metadata.yaml | entity_Scenario | From f94cb18fe9949cd515beccd109737ef49159b8f6 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:01:23 -0500 Subject: [PATCH 073/144] updated service level readme --- .../example_code/entityresolution/README.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 javav2/example_code/entityresolution/README.md diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md new file mode 100644 index 00000000000..fa5c9287dd5 --- /dev/null +++ b/javav2/example_code/entityresolution/README.md @@ -0,0 +1,123 @@ +# AWS Entity Resolution code examples for the SDK for Java 2.x + +## Overview + +Shows how to use the AWS SDK for Java 2.x to work with AWS Entity Resolution. + + + + +_AWS Entity Resolution helps organizations extract, link, and organize information from multiple data sources._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav2` folder. + + + + + +### Get started + +- [Hello AWS Entity Resolution](src/main/java/com/example/entity/HelloEntityResoultion.java#L19) (`listMatchingWorkflows`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](src/main/java/com/example/entity/scenario/EntityResScenario.java) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L305) +- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L333) +- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L183) +- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) +- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L250) +- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L223) +- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L136) +- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L276) +- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L413) +- [entityresolution_DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS Entity Resolution + +This example shows you how to get started using AWS Entity Resolution. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create Schema Mapping. +- Create an AWS Entity Resolution workflow. +- Start the matching job for the workflow. +- Get details for the matching job. +- Get Schema Mapping. +- List all Schema Mappings. +- Tag the Schema Mapping resource. +- Delete the AWS Entity Resolution Assets. + + + + + + + + + +### 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 `javav2` folder. + + + + + + +## Additional resources + +- [AWS Entity Resolution User Guide](https://docs.aws.amazon.com/entityresolution/latest/userguide/what-is-service.html) +- [AWS Entity Resolution API Reference](https://docs.aws.amazon.com/entityresolution/latest/apireference/Welcome.html) +- [SDK for Java 2.x AWS Entity Resolution reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/entityresolution/package-summary.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 From d2410b4f5237155e611ddb9c8fa00f8c5b4daca1 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:05:29 -0500 Subject: [PATCH 074/144] updated service level readme --- .doc_gen/metadata/entityresolution_metadata.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index c25218b5164..9906a5edb0f 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -25,7 +25,7 @@ entityresolution_DeleteSchemaMapping: snippet_tags: - entityres.java2_delete_mappings.main services: - entityresolution: {entityresolution_DeleteSchemaMapping} + entityresolution: {DeleteSchemaMapping} entityresolution_TagEntityResource: languages: Java: @@ -159,4 +159,5 @@ entityresolution_Scenario: snippet_tags: - entityres.java2_actions.main services: - entityresolution: {} \ No newline at end of file + entityresolution: {} + \ No newline at end of file From 3efcb6fa87ed619dc2a1775200218e7b9cc6ba99 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:08:38 -0500 Subject: [PATCH 075/144] updated service level readme --- javav2/example_code/entityresolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md index fa5c9287dd5..5ea507e27b2 100644 --- a/javav2/example_code/entityresolution/README.md +++ b/javav2/example_code/entityresolution/README.md @@ -49,12 +49,12 @@ Code excerpts that show you how to call individual service functions. - [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L333) - [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L183) - [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) +- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) - [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L250) - [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L223) - [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L136) - [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L276) - [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L413) -- [entityresolution_DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) From b2420cb16f7f43524201b5dbcdc254f077148b8c Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:10:42 -0500 Subject: [PATCH 076/144] updated service level readme --- .doc_gen/metadata/entityresolution_metadata.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 9906a5edb0f..b318b6c2a41 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -160,4 +160,3 @@ entityresolution_Scenario: - entityres.java2_actions.main services: entityresolution: {} - \ No newline at end of file From da696099970498acbc7ac2155cefd2b4406d410c Mon Sep 17 00:00:00 2001 From: Dennis Traub Date: Tue, 25 Feb 2025 22:19:44 +0100 Subject: [PATCH 077/144] JavaScript examples for Amazon Nova and Amazon Nova Canvas (#7253) * Add JavaScript examples for Amazon Nova and Amazon Nova Canvas * Update dependencies --------- Co-authored-by: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> --- .../metadata/bedrock-runtime_metadata.yaml | 24 +++++ .../example_code/bedrock-runtime/.gitignore | 1 + .../example_code/bedrock-runtime/README.md | 9 ++ .../models/amazonNovaCanvas/invokeModel.js | 93 +++++++++++++++++++ .../models/amazonNovaText/converse.js | 68 ++++++++++++++ .../models/amazonNovaText/converseStream.js | 75 +++++++++++++++ .../example_code/bedrock-runtime/package.json | 4 +- .../tests/converse.integration.test.js | 23 ++--- .../tests/converse_stream.integration.test.js | 21 +++-- .../image_generation.integration.test.js | 13 +++ .../bedrock-runtime/tests/test_tools.js | 2 +- .../bedrock-runtime/utils/image-creation.js | 54 +++++++++++ 12 files changed, 363 insertions(+), 24 deletions(-) create mode 100644 javascriptv3/example_code/bedrock-runtime/models/amazonNovaCanvas/invokeModel.js create mode 100644 javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converse.js create mode 100644 javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converseStream.js create mode 100644 javascriptv3/example_code/bedrock-runtime/tests/image_generation.integration.test.js create mode 100644 javascriptv3/example_code/bedrock-runtime/utils/image-creation.js diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index e0c702bd667..36cb8c49cab 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -100,6 +100,14 @@ bedrock-runtime_Converse_AmazonNovaText: - description: Send a text message to Amazon Nova, using Bedrock's Converse API. snippet_tags: - bedrock-runtime.java2.Converse_AmazonNovaText + JavaScript: + versions: + - sdk_version: 3 + github: javascriptv3/example_code/bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova, using Bedrock's Converse API. + snippet_tags: + - javascript.v3.bedrock-runtime.Converse_AmazonTitanText .NET: versions: - sdk_version: 3 @@ -423,6 +431,14 @@ bedrock-runtime_ConverseStream_AmazonNovaText: - description: Send a text message to Amazon Nova using Bedrock's Converse API and process the response stream in real-time. snippet_tags: - bedrock-runtime.java2.ConverseStream_AmazonNovaText + JavaScript: + versions: + - sdk_version: 3 + github: javascriptv3/example_code/bedrock-runtime + excerpts: + - description: Send a text message to Amazon Nova using Bedrock's Converse API and process the response stream in real-time. + snippet_tags: + - javascript.v3.bedrock-runtime.Converse_Mistral .NET: versions: - sdk_version: 3 @@ -1235,6 +1251,14 @@ bedrock-runtime_InvokeModel_AmazonNovaImageGeneration: - description: Create an image with Amazon Nova Canvas. snippet_tags: - bedrock-runtime.java2.InvokeModel_AmazonNovaImageGeneration + JavaScript: + versions: + - sdk_version: 3 + github: javascriptv3/example_code/bedrock-runtime + excerpts: + - description: Create an image with Amazon Nova Canvas. + snippet_tags: + - javascript.v3.bedrock-runtime.InvokeModel_AmazonNovaImageGeneration .NET: versions: - sdk_version: 3 diff --git a/javascriptv3/example_code/bedrock-runtime/.gitignore b/javascriptv3/example_code/bedrock-runtime/.gitignore index e90ea2eff59..0d1d9b21219 100644 --- a/javascriptv3/example_code/bedrock-runtime/.gitignore +++ b/javascriptv3/example_code/bedrock-runtime/.gitignore @@ -1 +1,2 @@ /tempx/ +/output/ diff --git a/javascriptv3/example_code/bedrock-runtime/README.md b/javascriptv3/example_code/bedrock-runtime/README.md index beb3f1cef10..078d3512168 100644 --- a/javascriptv3/example_code/bedrock-runtime/README.md +++ b/javascriptv3/example_code/bedrock-runtime/README.md @@ -50,6 +50,15 @@ functions within the same service. - [Converse](models/ai21LabsJurassic2/converse.js#L4) - [InvokeModel](models/ai21LabsJurassic2/invoke_model.js) +### Amazon Nova + +- [Converse](models/amazonTitanText/converse.js#L4) +- [ConverseStream](models/mistral/converse.js#L4) + +### Amazon Nova Canvas + +- [InvokeModel](models/amazonNovaCanvas/invokeModel.js#L4) + ### Amazon Titan Text - [Converse](models/amazonTitanText/converse.js#L4) diff --git a/javascriptv3/example_code/bedrock-runtime/models/amazonNovaCanvas/invokeModel.js b/javascriptv3/example_code/bedrock-runtime/models/amazonNovaCanvas/invokeModel.js new file mode 100644 index 00000000000..897ff67dc97 --- /dev/null +++ b/javascriptv3/example_code/bedrock-runtime/models/amazonNovaCanvas/invokeModel.js @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[javascript.v3.bedrock-runtime.InvokeModel_AmazonNovaImageGeneration] + +import { + BedrockRuntimeClient, + InvokeModelCommand, +} from "@aws-sdk/client-bedrock-runtime"; +import { saveImage } from "../../utils/image-creation.js"; +import { fileURLToPath } from "node:url"; + +/** + * This example demonstrates how to use Amazon Nova Canvas to generate images. + * It shows how to: + * - Set up the Amazon Bedrock runtime client + * - Configure the image generation parameters + * - Send a request to generate an image + * - Process the response and handle the generated image + * + * @returns {Promise} Base64-encoded image data + */ +export const invokeModel = async () => { + // Step 1: Create the Amazon Bedrock runtime client + // Credentials will be automatically loaded from the environment + const client = new BedrockRuntimeClient({ region: "us-east-1" }); + + // Step 2: Specify which model to use + // For the latest available models, see: + // https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html + const modelId = "amazon.nova-canvas-v1:0"; + + // Step 3: Configure the request payload + // First, set the main parameters: + // - prompt: Text description of the image to generate + // - seed: Random number for reproducible generation (0 to 858,993,459) + const prompt = "A stylized picture of a cute old steampunk robot"; + const seed = Math.floor(Math.random() * 858993460); + + // Then, create the payload using the following structure: + // - taskType: TEXT_IMAGE (specifies text-to-image generation) + // - textToImageParams: Contains the text prompt + // - imageGenerationConfig: Contains optional generation settings (seed, quality, etc.) + // For a list of available request parameters, see: + // https://docs.aws.amazon.com/nova/latest/userguide/image-gen-req-resp-structure.html + const payload = { + taskType: "TEXT_IMAGE", + textToImageParams: { + text: prompt, + }, + imageGenerationConfig: { + seed, + quality: "standard", + }, + }; + + // Step 4: Send and process the request + // - Embed the payload in a request object + // - Send the request to the model + // - Extract and return the generated image data from the response + try { + const request = { + modelId, + body: JSON.stringify(payload), + }; + const response = await client.send(new InvokeModelCommand(request)); + + const decodedResponseBody = new TextDecoder().decode(response.body); + // The response includes an array of base64-encoded PNG images + /** @type {{images: string[]}} */ + const responseBody = JSON.parse(decodedResponseBody); + return responseBody.images[0]; // Base64-encoded image data + } catch (error) { + console.error(`ERROR: Can't invoke '${modelId}'. Reason: ${error.message}`); + throw error; + } +}; + +// If run directly, execute the example and save the generated image +if (process.argv[1] === fileURLToPath(import.meta.url)) { + console.log("Generating image. This may take a few seconds..."); + invokeModel() + .then(async (imageData) => { + const imagePath = await saveImage(imageData, "nova-canvas"); + // Example path: javascriptv3/example_code/bedrock-runtime/output/nova-canvas/image-01.png + console.log(`Image saved to: ${imagePath}`); + }) + .catch((error) => { + console.error("Execution failed:", error); + process.exitCode = 1; + }); +} +// snippet-end:[javascript.v3.bedrock-runtime.InvokeModel_AmazonNovaImageGeneration] diff --git a/javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converse.js b/javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converse.js new file mode 100644 index 00000000000..23c8d17dd45 --- /dev/null +++ b/javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converse.js @@ -0,0 +1,68 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[javascript.v3.bedrock-runtime.Converse_AmazonNovaText] +// This example demonstrates how to use the Amazon Nova foundation models to generate text. +// It shows how to: +// - Set up the Amazon Bedrock runtime client +// - Create a message +// - Configure and send a request +// - Process the response + +import { + BedrockRuntimeClient, + ConversationRole, + ConverseCommand, +} from "@aws-sdk/client-bedrock-runtime"; + +// Step 1: Create the Amazon Bedrock runtime client +// Credentials will be automatically loaded from the environment +const client = new BedrockRuntimeClient({ region: "us-east-1" }); + +// Step 2: Specify which model to use: +// Available Amazon Nova models and their characteristics: +// - Amazon Nova Micro: Text-only model optimized for lowest latency and cost +// - Amazon Nova Lite: Fast, low-cost multimodal model for image, video, and text +// - Amazon Nova Pro: Advanced multimodal model balancing accuracy, speed, and cost +// +// For the most current model IDs, see: +// https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html +const modelId = "amazon.nova-lite-v1:0"; + +// Step 3: Create the message +// The message includes the text prompt and specifies that it comes from the user +const inputText = + "Describe the purpose of a 'hello world' program in one line."; +const message = { + content: [{ text: inputText }], + role: ConversationRole.USER, +}; + +// Step 4: Configure the request +// Optional parameters to control the model's response: +// - maxTokens: maximum number of tokens to generate +// - temperature: randomness (max: 1.0, default: 0.7) +// OR +// - topP: diversity of word choice (max: 1.0, default: 0.9) +// Note: Use either temperature OR topP, but not both +const request = { + modelId, + messages: [message], + inferenceConfig: { + maxTokens: 500, // The maximum response length + temperature: 0.5, // Using temperature for randomness control + //topP: 0.9, // Alternative: use topP instead of temperature + }, +}; + +// Step 5: Send and process the request +// - Send the request to the model +// - Extract and return the generated text from the response +try { + const response = await client.send(new ConverseCommand(request)); + console.log(response.output.message.content[0].text); +} catch (error) { + console.error(`ERROR: Can't invoke '${modelId}'. Reason: ${error.message}`); + throw error; +} +// snippet-end:[javascript.v3.bedrock-runtime.Converse_AmazonNovaText] diff --git a/javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converseStream.js b/javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converseStream.js new file mode 100644 index 00000000000..5941c783f37 --- /dev/null +++ b/javascriptv3/example_code/bedrock-runtime/models/amazonNovaText/converseStream.js @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[javascript.v3.bedrock-runtime.ConverseStream_AmazonNovaText] +// This example demonstrates how to use the Amazon Nova foundation models +// to generate streaming text responses. +// It shows how to: +// - Set up the Amazon Bedrock runtime client +// - Create a message +// - Configure a streaming request +// - Process the streaming response + +import { + BedrockRuntimeClient, + ConversationRole, + ConverseStreamCommand, +} from "@aws-sdk/client-bedrock-runtime"; + +// Step 1: Create the Amazon Bedrock runtime client +// Credentials will be automatically loaded from the environment +const client = new BedrockRuntimeClient({ region: "us-east-1" }); + +// Step 2: Specify which model to use +// Available Amazon Nova models and their characteristics: +// - Amazon Nova Micro: Text-only model optimized for lowest latency and cost +// - Amazon Nova Lite: Fast, low-cost multimodal model for image, video, and text +// - Amazon Nova Pro: Advanced multimodal model balancing accuracy, speed, and cost +// +// For the most current model IDs, see: +// https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html +const modelId = "amazon.nova-lite-v1:0"; + +// Step 3: Create the message +// The message includes the text prompt and specifies that it comes from the user +const inputText = + "Describe the purpose of a 'hello world' program in one paragraph"; +const message = { + content: [{ text: inputText }], + role: ConversationRole.USER, +}; + +// Step 4: Configure the streaming request +// Optional parameters to control the model's response: +// - maxTokens: maximum number of tokens to generate +// - temperature: randomness (max: 1.0, default: 0.7) +// OR +// - topP: diversity of word choice (max: 1.0, default: 0.9) +// Note: Use either temperature OR topP, but not both +const request = { + modelId, + messages: [message], + inferenceConfig: { + maxTokens: 500, // The maximum response length + temperature: 0.5, // Using temperature for randomness control + //topP: 0.9, // Alternative: use topP instead of temperature + }, +}; + +// Step 5: Send and process the streaming request +// - Send the request to the model +// - Process each chunk of the streaming response +try { + const response = await client.send(new ConverseStreamCommand(request)); + + for await (const chunk of response.stream) { + if (chunk.contentBlockDelta) { + // Print each text chunk as it arrives + process.stdout.write(chunk.contentBlockDelta.delta?.text || ""); + } + } +} catch (error) { + console.error(`ERROR: Can't invoke '${modelId}'. Reason: ${error.message}`); + process.exitCode = 1; +} +// snippet-end:[javascript.v3.bedrock-runtime.ConverseStream_AmazonNovaText] diff --git a/javascriptv3/example_code/bedrock-runtime/package.json b/javascriptv3/example_code/bedrock-runtime/package.json index dba0f51ad9c..13b0a0d8350 100644 --- a/javascriptv3/example_code/bedrock-runtime/package.json +++ b/javascriptv3/example_code/bedrock-runtime/package.json @@ -8,9 +8,9 @@ "integration-test": "vitest run integration --reporter=junit --outputFile=test_results/bedrock-runtime-test-results.junit.xml" }, "devDependencies": { - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "dependencies": { - "@aws-sdk/client-bedrock-runtime": "^3.658.1" + "@aws-sdk/client-bedrock-runtime": "^3.751.0" } } diff --git a/javascriptv3/example_code/bedrock-runtime/tests/converse.integration.test.js b/javascriptv3/example_code/bedrock-runtime/tests/converse.integration.test.js index db5ac65d7c6..49cf7e73591 100644 --- a/javascriptv3/example_code/bedrock-runtime/tests/converse.integration.test.js +++ b/javascriptv3/example_code/bedrock-runtime/tests/converse.integration.test.js @@ -8,18 +8,19 @@ describe("Converse with text generation models", () => { const baseDirectory = path.join(__dirname, "..", "models"); const fileName = "converse.js"; - const subdirectories = [ - "ai21LabsJurassic2", - "amazonTitanText", - "anthropicClaude", - "cohereCommand", - "metaLlama", - "mistral", - ]; + const models = { + ai21LabsJurassic2: "AI21 Labs Jurassic-2", + amazonNovaText: "Amazon Nova", + amazonTitanText: "Amazon Titan", + anthropicClaude: "Anthropic Claude", + cohereCommand: "Cohere Command", + metaLlama: "Meta Llama", + mistral: "Mistral", + }; - test.each(subdirectories)( - "should invoke the model and return text", - async (subdirectory) => { + test.each(Object.entries(models).map(([sub, name]) => [name, sub]))( + "should invoke %s and return text", + async (_, subdirectory) => { const script = path.join(baseDirectory, subdirectory, fileName); const consoleLogSpy = vi.spyOn(console, "log"); diff --git a/javascriptv3/example_code/bedrock-runtime/tests/converse_stream.integration.test.js b/javascriptv3/example_code/bedrock-runtime/tests/converse_stream.integration.test.js index 64d964cccd0..916e976e803 100644 --- a/javascriptv3/example_code/bedrock-runtime/tests/converse_stream.integration.test.js +++ b/javascriptv3/example_code/bedrock-runtime/tests/converse_stream.integration.test.js @@ -9,17 +9,18 @@ describe("ConverseStream with text generation models", () => { const fileName = "converseStream.js"; const baseDirectory = path.join(__dirname, "..", "models"); - const subdirectories = [ - "amazonTitanText", - "anthropicClaude", - "cohereCommand", - "metaLlama", - "mistral", - ]; + const models = { + amazonNovaText: "Amazon Nova", + amazonTitanText: "Amazon Titan", + anthropicClaude: "Anthropic Claude", + cohereCommand: "Cohere Command", + metaLlama: "Meta Llama", + mistral: "Mistral", + }; - test.each(subdirectories)( - "should invoke the model and return text", - async (subdirectory) => { + test.each(Object.entries(models).map(([sub, name]) => [name, sub]))( + "should invoke %s and return text", + async (_, subdirectory) => { let output = ""; const outputStream = new Writable({ write(/** @type string */ chunk, encoding, callback) { diff --git a/javascriptv3/example_code/bedrock-runtime/tests/image_generation.integration.test.js b/javascriptv3/example_code/bedrock-runtime/tests/image_generation.integration.test.js new file mode 100644 index 00000000000..fccb5495126 --- /dev/null +++ b/javascriptv3/example_code/bedrock-runtime/tests/image_generation.integration.test.js @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it } from "vitest"; +import { invokeModel } from "../models/amazonNovaCanvas/invokeModel.js"; +import { expectToBeANonEmptyString } from "./test_tools.js"; + +describe("Invoking Amazon Nova Canvas", () => { + it("should return a response", async () => { + const response = await invokeModel(); + expectToBeANonEmptyString(response); + }); +}); diff --git a/javascriptv3/example_code/bedrock-runtime/tests/test_tools.js b/javascriptv3/example_code/bedrock-runtime/tests/test_tools.js index 7c12f2de8d2..5922dc95386 100644 --- a/javascriptv3/example_code/bedrock-runtime/tests/test_tools.js +++ b/javascriptv3/example_code/bedrock-runtime/tests/test_tools.js @@ -10,5 +10,5 @@ import { expect } from "vitest"; */ export const expectToBeANonEmptyString = (string) => { expect(typeof string).toBe("string"); - expect(string.length).not.toBe(0); + expect(string).not.toHaveLength(0); }; diff --git a/javascriptv3/example_code/bedrock-runtime/utils/image-creation.js b/javascriptv3/example_code/bedrock-runtime/utils/image-creation.js new file mode 100644 index 00000000000..2c3ae971e54 --- /dev/null +++ b/javascriptv3/example_code/bedrock-runtime/utils/image-creation.js @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { mkdir, readdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Creates the output directory if it doesn't exist and gets the next available image number + * @param {string} outputDir - The directory path where images will be saved + * @returns {Promise} The next available image number + */ +async function prepareOutputDirectory(outputDir) { + try { + await mkdir(outputDir, { recursive: true }); + const files = await readdir(outputDir); + + // Find the highest existing image number + const numbers = files + .filter((file) => file.match(/^image-\d+\.png$/)) + .map((file) => Number.parseInt(file.match(/^image-(\d+)\.png$/)[1])); + + return numbers.length > 0 ? Math.max(...numbers) + 1 : 1; + } catch (error) { + console.error(`Error preparing output directory: ${error.message}`); + throw error; + } +} + +/** + * Saves an image to the output directory with automatic numbering + * @param {string} imageData - Base64-encoded image data + * @param {string} modelName - Name of the model used to generate the image + * @returns {Promise} The full path where the image was saved + */ +export async function saveImage(imageData, modelName) { + // Set up the output directory path relative to this utility script + const utilityDir = dirname(fileURLToPath(import.meta.url)); + const outputDir = join(utilityDir, "..", "output", modelName); + + // Get the next available image number + const imageNumber = await prepareOutputDirectory(outputDir); + + // Create the image filename with padded number + const paddedNumber = imageNumber.toString().padStart(2, "0"); + const filename = `image-${paddedNumber}.png`; + const fullPath = join(outputDir, filename); + + // Save the image + const buffer = Buffer.from(imageData, "base64"); + await writeFile(fullPath, buffer); + + return fullPath; +} From 3adb8c1f78e44230b8208cfe40d5370794608968 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 18:24:21 -0500 Subject: [PATCH 078/144] updated exception hanlder to stop program if an exception is thrown --- .../entity/scenario/EntityResScenario.java | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 9899c69a34e..68248c12e02 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -111,7 +111,12 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); logger.info(csv); waitForInputToContinue(scanner); - actions.uploadInputData(glueBucketName, json, csv); + try { + actions.uploadInputData(glueBucketName, json, csv); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } logger.info("The JSON objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); logger.info(DASHES); @@ -135,7 +140,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The JSON schema mapping name is " + jsonSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } try { @@ -144,7 +149,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); logger.info(DASHES); @@ -168,7 +173,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } waitForInputToContinue(scanner); @@ -181,7 +187,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -193,7 +200,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.getMatchingJobAsync(jobId, workflowName).join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } logger.info(DASHES); @@ -205,14 +213,20 @@ Amazon Web Services (AWS) that helps organizations extract, link, and jsonSchemaMappingArn = response.schemaArn(); logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { - logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); + logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); + return; } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("6. List Schema Mappings."); - actions.ListSchemaMappings(); + try { + actions.ListSchemaMappings(); + } catch (CompletionException ce) { + logger.error("Error retrieving schema mapping: " + ce.getCause().getMessage()); + return; + } waitForInputToContinue(scanner); logger.info(DASHES); @@ -225,7 +239,13 @@ Amazon Web Services (AWS) that helps organizations extract, link, and In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, the SchemaMapping is tagged. """); - actions.tagEntityResource(jsonSchemaMappingArn).join(); + try { + actions.tagEntityResource(jsonSchemaMappingArn).join(); + } catch (CompletionException ce) { + logger.error("Error tagging the resource: " + ce.getCause().getMessage()); + return; + } + waitForInputToContinue(scanner); logger.info(DASHES); @@ -280,6 +300,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } try { @@ -289,6 +310,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Both schema mappings were deleted successfully!"); } catch (RuntimeException e) { logger.error("Error deleting schema mapping: {}", e.getMessage()); + return; } waitForInputToContinue(scanner); @@ -304,6 +326,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); + return; } } else { From e99c2f07b01af15b0600a6092268bf2c4bc5a902 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 26 Feb 2025 19:25:10 -0500 Subject: [PATCH 079/144] updated exception hanlder to stop program if an exception is thrown --- .../entity/scenario/EntityResActions.java | 492 +++++++++++------- .../entity/scenario/EntityResScenario.java | 141 ++++- 2 files changed, 425 insertions(+), 208 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 5c6e24c6016..04f84f66971 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -12,13 +12,17 @@ import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowRequest; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowResponse; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteSchemaMappingRequest; +import software.amazon.awssdk.services.entityresolution.model.DeleteSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; @@ -28,9 +32,12 @@ import software.amazon.awssdk.services.entityresolution.model.OutputSource; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; +import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; import software.amazon.awssdk.services.entityresolution.model.SchemaAttributeType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.TagResourceResponse; +import software.amazon.awssdk.services.entityresolution.model.ValidationException; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -41,6 +48,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { @@ -60,23 +68,23 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return entityResolutionAsyncClient; } @@ -91,23 +99,23 @@ public static S3AsyncClient getS3AsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); s3AsyncClient = S3AsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return s3AsyncClient; } @@ -118,17 +126,32 @@ public static S3AsyncClient getS3AsyncClient() { * * @param schemaName the name of the schema to delete * @return a {@link CompletableFuture} that completes when the schema mapping is deleted successfully, - * or throws a {@link RuntimeException} if the deletion fails + * or throws a {@link RuntimeException} if the deletion fails */ - public CompletableFuture deleteSchemaMappingAsync(String schemaName) { + public CompletableFuture deleteSchemaMappingAsync(String schemaName) { DeleteSchemaMappingRequest request = DeleteSchemaMappingRequest.builder() .schemaName(schemaName) .build(); return getResolutionAsyncClient().deleteSchemaMapping(request) - .thenRun(() -> logger.info("Schema mapping '{}' deleted successfully.", schemaName)) - .exceptionally(ex -> { - throw new RuntimeException("Failed to delete schema mapping: " + schemaName, ex); + .whenComplete((response, exception) -> { + if (response != null) { + // Successfully deleted the schema mapping, log the success message. + logger.info("Schema mapping '{}' deleted successfully.", schemaName); + } else { + // Ensure exception is not null before accessing its cause. + if (exception == null) { + throw new CompletionException("An unknown error occurred while deleting the schema mapping.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The schema mapping was not found to delete: " + schemaName, cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to delete schema mapping: " + schemaName, exception); + } }); } // snippet-end:[entityres.java2_delete_mappings.main] @@ -140,14 +163,14 @@ public CompletableFuture deleteSchemaMappingAsync(String schemaName) { */ public void ListSchemaMappings() { ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() - .build(); + .build(); ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); // Iterate through the pages of results CompletableFuture future = paginator.subscribe(response -> { response.schemaList().forEach(schemaMapping -> - logger.info("Schema Mapping Name: " + schemaMapping.schemaName()) + logger.info("Schema Mapping Name: " + schemaMapping.schemaName()) ); }); @@ -157,7 +180,6 @@ public void ListSchemaMappings() { // snippet-end:[entityres.java2_list_mappings.main] // snippet-start:[entityres.java2_delete_matching_workflow.main] - /** * Asynchronously deletes a workflow with the specified name. * @@ -165,18 +187,29 @@ public void ListSchemaMappings() { * @return a {@link CompletableFuture} that completes when the workflow has been deleted * @throws RuntimeException if the deletion of the workflow fails */ - public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { + public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { DeleteMatchingWorkflowRequest request = DeleteMatchingWorkflowRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().deleteMatchingWorkflow(request) - .thenAccept(response -> { - // No response object, just log success - }) - .exceptionally(exception -> { - throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); - }); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("{} was deleted", workflowName ); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while deleting the workflow.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The workflow to delete was not found.", cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to delete workflow: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_delete_matching_workflow.main] @@ -191,32 +224,42 @@ public CompletableFuture createSchemaMappingAsync(S List schemaAttributes = null; if (schemaName.startsWith("json")) { schemaAttributes = List.of( - SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), - SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), - SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build() + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build() ); } else { schemaAttributes = List.of( - SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), - SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), - SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build(), - SchemaInputAttribute.builder().fieldName("phone").type(SchemaAttributeType.PROVIDER_ID).subType("STRING").build() + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build(), + SchemaInputAttribute.builder().fieldName("phone").type(SchemaAttributeType.PROVIDER_ID).subType("STRING").build() ); } CreateSchemaMappingRequest request = CreateSchemaMappingRequest.builder() - .schemaName(schemaName) - .mappedInputFields(schemaAttributes) - .build(); + .schemaName(schemaName) + .mappedInputFields(schemaAttributes) + .build(); return getResolutionAsyncClient().createSchemaMapping(request) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("[{}] schema mapping Created Successfully!", schemaName); - } else { - throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("[{}] schema mapping Created Successfully!", schemaName); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while creating the schema mapping.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ConflictException) { + throw new CompletionException("A conflicting schema mapping already exists. Resolve conflicts before proceeding.", cause); } - }); + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to create schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_create_schema.main] @@ -226,29 +269,38 @@ public CompletableFuture createSchemaMappingAsync(S * * @param schemaName the name of the schema to retrieve the mapping for * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation - * is complete + * is complete * @throws RuntimeException if the schema mapping retrieval fails */ public CompletableFuture getSchemaMappingAsync(String schemaName) { GetSchemaMappingRequest mappingRequest = GetSchemaMappingRequest.builder() - .schemaName(schemaName) - .build(); + .schemaName(schemaName) + .build(); return getResolutionAsyncClient().getSchemaMapping(mappingRequest) - .whenComplete((response, exception) -> { - if (response != null) { - response.mappedInputFields().forEach(attribute -> - logger.info("Attribute Name: " + attribute.fieldName() + - ", Attribute Type: " + attribute.type().toString())); - } else { - throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + response.mappedInputFields().forEach(attribute -> + logger.info("Attribute Name: " + attribute.fieldName() + + ", Attribute Type: " + attribute.type().toString())); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while getting schema mapping.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The requested schema mapping was not found.", cause); } - }); + + // Wrap other exceptions in a CompletionException with the message. + throw new CompletionException("Failed to get schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_get_schema_mapping.main] // snippet-start:[entityres.java2_get_job.main] - /** * Asynchronously retrieves a matching job based on the provided job ID and workflow name. * @@ -256,20 +308,33 @@ public CompletableFuture getSchemaMappingAsync(String * @param workflowName the name of the workflow associated with the job * @return a {@link CompletableFuture} that completes when the job information is available or an exception occurs */ - public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { + public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenAccept(response -> { + .whenComplete((response, exception) -> { + if (response != null) { + // Successfully fetched the matching job details, log the job status. logger.info("Job status: " + response.status()); logger.info("Job details: " + response.toString()); - }) - .exceptionally(ex -> { - throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); - }); + } else { + // Handle the case where there is an exception. + if (exception == null) { + throw new CompletionException("An unknown error occurred while fetching the matching job.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The requested job could not be found.", cause); + } + + // Wrap other exceptions in a CompletionException with the message. + throw new CompletionException("Error fetching matching job: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_get_job.main] @@ -280,168 +345,230 @@ public CompletableFuture getMatchingJobAsync(String jobId, String workflow * * @param workflowName the name of the workflow for which to start the matching job * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty - * string if the operation fails + * string if the operation fails */ public CompletableFuture startMatchingJobAsync(String workflowName) { StartMatchingJobRequest jobRequest = StartMatchingJobRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().startMatchingJob(jobRequest) - .whenComplete((response, exception) -> { - if (response != null) { - // Get the job ID from the response - String jobId = response.jobId(); - logger.info("Job ID: " + jobId); - } else { - // Handle the exception if the response is null - throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + String jobId = response.jobId(); + logger.info("Job ID: " + jobId); + } else { + // Ensure exception is not null before accessing its cause. + if (exception == null) { + throw new CompletionException("An unknown error occurred while starting the job.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ConflictException) { + throw new CompletionException("The job is already running. Resolve conflicts before starting a new job.", cause); } - }) - .thenApply(response -> response != null ? response.jobId() : ""); + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to start the job: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response != null ? response.jobId() : ""); } // snippet-end:[entityres.java2_start_job.main] // snippet-start:[entityres.java2_check_matching_workflow.main] - /** * Checks the status of a workflow asynchronously. * * @param jobId the ID of the job to check * @param workflowName the name of the workflow to check * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed - * successfully + * successfully */ - public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { + public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenApply(response -> { - logger.info("\nJob status: " + response.status()); - return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); - }) - .exceptionally(exception -> { - logger.info("Error checking workflow status: " + exception.getMessage()); - return false; - }); + .whenComplete((response, exception) -> { + if (response != null) { + // Process the response and log the job status. + logger.info("Job status: " + response.status()); + } else { + // Ensure exception is not null before accessing its cause. + if (exception == null) { + throw new CompletionException("An unknown error occurred while checking job status.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The requested resource was not found while checking the job status.", cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to check job status: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_check_matching_workflow.main] // snippet-start:[entityres.java2_create_matching_workflow.main] - /** * Creates an asynchronous CompletableFuture to manage the creation of a matching workflow. * - * @param roleARN the AWS IAM role ARN to be used for the workflow execution - * @param workflowName the name of the workflow to be created - * @param outputBucket the S3 bucket path where the workflow output will be stored - * @param jsonGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source + * @param roleARN the AWS IAM role ARN to be used for the workflow execution + * @param workflowName the name of the workflow to be created + * @param outputBucket the S3 bucket path where the workflow output will be stored + * @param jsonGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source * @param jsonErSchemaMappingName the name of the schema to be used for the input source * @return a CompletableFuture that, when completed, will return the ARN of the created workflow */ public CompletableFuture createMatchingWorkflowAsync( - String roleARN - , String workflowName - , String outputBucket - , String jsonGlueTableArn - , String jsonErSchemaMappingName - , String csvGlueTableArn - , String csvErSchemaMappingName) { + String roleARN + , String workflowName + , String outputBucket + , String jsonGlueTableArn + , String jsonErSchemaMappingName + , String csvGlueTableArn + , String csvErSchemaMappingName) { InputSource jsonInputSource = InputSource.builder() - .inputSourceARN(jsonGlueTableArn) - .schemaName(jsonErSchemaMappingName) - .applyNormalization(false) - .build(); + .inputSourceARN(jsonGlueTableArn) + .schemaName(jsonErSchemaMappingName) + .applyNormalization(false) + .build(); InputSource csvInputSource = InputSource.builder() - .inputSourceARN(csvGlueTableArn) - .schemaName(csvErSchemaMappingName) - .applyNormalization(false) - .build(); + .inputSourceARN(csvGlueTableArn) + .schemaName(csvErSchemaMappingName) + .applyNormalization(false) + .build(); OutputAttribute idOutputAttribute = OutputAttribute.builder() - .name("id") - .build(); + .name("id") + .build(); OutputAttribute nameOutputAttribute = OutputAttribute.builder() - .name("name") - .build(); + .name("name") + .build(); OutputAttribute emailOutputAttribute = OutputAttribute.builder() - .name("email") - .build(); + .name("email") + .build(); OutputAttribute phoneOutputAttribute = OutputAttribute.builder() - .name("phone") - .build(); + .name("phone") + .build(); OutputSource outputSource = OutputSource.builder() - .outputS3Path("s3://" + outputBucket + "/eroutput") - .output(idOutputAttribute, nameOutputAttribute, emailOutputAttribute, phoneOutputAttribute) - .applyNormalization(false) - .build(); + .outputS3Path("s3://" + outputBucket + "/eroutput") + .output(idOutputAttribute, nameOutputAttribute, emailOutputAttribute, phoneOutputAttribute) + .applyNormalization(false) + .build(); ResolutionTechniques resolutionType = ResolutionTechniques.builder() - .resolutionType(ResolutionType.ML_MATCHING) - .build(); + .resolutionType(ResolutionType.ML_MATCHING) + .build(); CreateMatchingWorkflowRequest workflowRequest = CreateMatchingWorkflowRequest.builder() - .roleArn(roleARN) - .description("Created by using the AWS SDK for Java") - .workflowName(workflowName) - .inputSourceConfig(List.of(jsonInputSource, csvInputSource)) - .outputSourceConfig(List.of(outputSource)) - .resolutionTechniques(resolutionType) - .build(); + .roleArn(roleARN) + .description("Created by using the AWS SDK for Java") + .workflowName(workflowName) + .inputSourceConfig(List.of(jsonInputSource, csvInputSource)) + .outputSourceConfig(List.of(outputSource)) + .resolutionTechniques(resolutionType) + .build(); return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("Workflow created successfully."); - } else { - throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Workflow created successfully."); + } else { + Throwable cause = exception.getCause(); + if (cause instanceof ValidationException) { + throw new CompletionException("Invalid request: Please check input parameters.", cause); } - }) - .thenApply(CreateMatchingWorkflowResponse::workflowArn); + + if (cause instanceof ConflictException) { + throw new CompletionException("A conflicting workflow already exists. Resolve conflicts before proceeding.", cause); + } + throw new CompletionException("Failed to create workflow: " + exception.getMessage(), exception); + } + }) + .thenApply(CreateMatchingWorkflowResponse::workflowArn); } // snippet-end:[entityres.java2_create_matching_workflow.main] // snippet-start:[entityres.java2_tag_resource.main] - /** * Tags the specified schema mapping ARN. * * @param schemaMappingARN the ARN of the schema mapping to tag */ - public CompletableFuture tagEntityResource(String schemaMappingARN) { + public CompletableFuture tagEntityResource(String schemaMappingARN) { Map tags = new HashMap<>(); tags.put("tag1", "tag1Value"); tags.put("tag2", "tag2Value"); TagResourceRequest request = TagResourceRequest.builder() - .resourceArn(schemaMappingARN) - .tags(tags) - .build(); + .resourceArn(schemaMappingARN) + .tags(tags) + .build(); return getResolutionAsyncClient().tagResource(request) - .thenAccept(response -> logger.info("Successfully tagged the resource.")) - .exceptionally(exception -> { - throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); - }); + .whenComplete((response, exception) -> { + if (response != null) { + // Successfully tagged the resource, log the success message. + logger.info("Successfully tagged the resource."); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while tagging the resource.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The resource to tag was not found.", cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to tag the resource: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_tag_resource.main] - public CompletableFuture getJobInfo(String workflowName, String jobId){ - return getResolutionAsyncClient().getMatchingJob(b -> b - .workflowName(workflowName) - .jobId(jobId)) - .thenApply(response -> response.metrics()); + // snippet-start:[entityres.java2_job_info.main] + public CompletableFuture getJobInfo(String workflowName, String jobId) { + return getResolutionAsyncClient().getMatchingJob(b -> b + .workflowName(workflowName) + .jobId(jobId)) + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Job metrics fetched successfully for jobId: " + jobId); + } else { + Throwable cause = exception.getCause(); + + + if (cause instanceof ResourceNotFoundException) { + // Handle validation errors if needed + throw new CompletionException("Invalid request: Job id was not found.", cause); + } + + if (cause instanceof ConflictException) { + // Handle conflict errors if needed + throw new CompletionException("A conflicting request occurred. Resolve conflicts before proceeding.", cause); + } + // Generic failure case + throw new CompletionException("Failed to fetch job info: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response.metrics()); // Extract job metrics } + // snippet-end:[entityres.java2_job_info.main] + /** * Uploads data to an Amazon S3 bucket asynchronously. * @@ -456,28 +583,29 @@ public void uploadInputData(String bucketName, String jsonData, String csvData) // Upload JSON data. String jsonKey = "jsonData/data.json"; PutObjectRequest jsonUploadRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(jsonKey) - .contentType("application/json") - .build(); + .bucket(bucketName) + .key(jsonKey) + .contentType("application/json") + .build(); CompletableFuture jsonUploadResponse = getS3AsyncClient().putObject(jsonUploadRequest, AsyncRequestBody.fromString(jsonData)); // Upload CSV data. String csvKey = "csvData/data.csv"; PutObjectRequest csvUploadRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(csvKey) - .contentType("text/csv") - .build(); + .bucket(bucketName) + .key(csvKey) + .contentType("text/csv") + .build(); CompletableFuture csvUploadResponse = getS3AsyncClient().putObject(csvUploadRequest, AsyncRequestBody.fromString(csvData)); CompletableFuture.allOf(jsonUploadResponse, csvUploadResponse) - .whenComplete((result, ex) -> { - if (ex != null) { - throw new RuntimeException("Failed to upload files", ex); - } - }).join(); + .whenComplete((result, ex) -> { + if (ex != null) { + // Wrap an AWS exception. + throw new CompletionException("Failed to upload files", ex); + } + }).join(); } // snippet-end:[entityres.java2_actions.main] diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 68248c12e02..5a11e7d4529 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -4,12 +4,19 @@ package com.example.entity.scenario; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.entityresolution.model.AccessDeniedException; +import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.JobMetrics; +import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; +import software.amazon.awssdk.services.entityresolution.model.ThrottlingException; +import software.amazon.awssdk.services.entityresolution.model.ValidationException; import java.util.Map; import java.util.Scanner; @@ -28,16 +35,17 @@ public class EntityResScenario { private static String glueBucketName; private static String workflowName = "workflow-" + UUID.randomUUID(); + private static String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); + private static String jsonSchemaMappingArn = null; + private static String csvSchemaMappingName = "csv-" + UUID.randomUUID(); + private static String roleARN; + private static String csvGlueTableArn; + private static String jsonGlueTableArn; + private static Scanner scanner = new Scanner(System.in); + + private static EntityResActions actions = new EntityResActions(); public static void main(String[] args) throws InterruptedException { - String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); - String jsonSchemaMappingArn = null; - String csvSchemaMappingName = "csv-" + UUID.randomUUID(); - String roleARN; - String csvGlueTableArn; - String jsonGlueTableArn; - - EntityResActions actions = new EntityResActions(); - Scanner scanner = new Scanner(System.in); + logger.info("Welcome to the AWS Entity Resolution Scenario."); logger.info(""" AWS Entity Resolution is a fully-managed machine learning service provided by @@ -88,6 +96,16 @@ Amazon Web Services (AWS) that helps organizations extract, link, and jsonGlueTableArn = outputsMap.get(JSON_GLUE_TABLE_ARN_KEY); logger.info(DASHES); waitForInputToContinue(scanner); + + try { + runScenario(); + + } catch (Exception ce) { + Throwable cause = ce.getCause(); + logger.error("An exception happened: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + } + private static void runScenario() throws InterruptedException { /* This JSON is a valid input for the AWS Entity Resolution service. The JSON represents an array of three objects, each containing an "id", "name", and "email" @@ -115,9 +133,23 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.uploadInputData(glueBucketName, json, csv); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + + if (cause == null) { + logger.error("Failed to upload input data: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ResourceNotFoundException) { + logger.error("The S3 bucket could not be found: {}", cause.getMessage(), cause); + } else { + logger.error("Failed to upload input data: {}", cause.getMessage(), cause); + } + + // Always wrap checked exceptions in a CompletionException + throw (cause instanceof RuntimeException) ? (RuntimeException) cause + : new CompletionException("Unhandled checked exception during input data upload", cause); } - logger.info("The JSON objects have been uploaded to the S3 bucket."); + logger.info("The JSON and CSV objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); logger.info(DASHES); @@ -133,14 +165,27 @@ Amazon Web Services (AWS) that helps organizations extract, link, and In this example, the schema mapping lines up with the fields in the JSON ans CSV objects. That is, it contains these fields: id, name, and email. """); - waitForInputToContinue(scanner); try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); jsonSchemaMappingName = response.schemaName(); logger.info("The JSON schema mapping name is " + jsonSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + + if (cause == null) { + logger.error("Failed to create JSON schema mapping: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ConflictException) { + logger.error("Schema mapping conflict detected: {}", cause.getMessage(), cause); + } else { + logger.error("Unexpected error while creating schema mapping: {}", cause.getMessage(), cause); + } + + // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException + throw (cause instanceof RuntimeException) ? (RuntimeException) cause + : new CompletionException("Unhandled checked exception in schema mapping creation", cause); } try { @@ -149,7 +194,21 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + + if (cause == null) { + logger.error("Failed to create CSV schema mapping: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ConflictException) { + logger.error("Schema mapping conflict detected: {}", cause.getMessage(), cause); + } else { + logger.error("Unexpected error while creating CSV schema mapping: {}", cause.getMessage(), cause); + } + + // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException + throw (cause instanceof RuntimeException) ? (RuntimeException) cause + : new CompletionException("Unhandled checked exception in schema mapping creation", cause); } waitForInputToContinue(scanner); logger.info(DASHES); @@ -173,8 +232,27 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; + + if (cause == null) { + // Log the exception and rethrow the CompletionException if no cause is found + logger.error("An unexpected error occurred: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ValidationException) { + logger.error("Validation error: {}", cause.getMessage(), cause); + } else if (cause instanceof ConflictException) { + logger.error("Workflow conflict detected: {}", cause.getMessage(), cause); + } else { + logger.error("Unexpected error: {}", ce.getMessage(), ce); + } + + // Rethrow as a RuntimeException if cause is a RuntimeException, else rethrow CompletionException + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new CompletionException("Unhandled exception occurred during workflow creation", cause); + } } waitForInputToContinue(scanner); @@ -187,8 +265,18 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; + + if (cause instanceof ConflictException) { + logger.error("Job conflict detected: {}", cause.getMessage(), cause); + } else if (cause instanceof RuntimeException) { + // Log and rethrow RuntimeException, ensuring it's captured in the first block + logger.error("Runtime error while starting the job: {}", cause.getMessage(), cause); + throw (RuntimeException) cause; // Rethrow RuntimeException for the first block to handle + } else { + logger.error("Unexpected error while starting the job: {}", ce.getMessage(), ce); + // For other checked exceptions, wrap them in a new CompletionException + throw new CompletionException("Unhandled checked exception while starting the job.", cause); + } } waitForInputToContinue(scanner); logger.info(DASHES); @@ -201,7 +289,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; + throw ce; } logger.info(DASHES); @@ -214,7 +302,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); - return; + throw ce; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -225,7 +313,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.ListSchemaMappings(); } catch (CompletionException ce) { logger.error("Error retrieving schema mapping: " + ce.getCause().getMessage()); - return; + throw ce; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -243,7 +331,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.tagEntityResource(jsonSchemaMappingArn).join(); } catch (CompletionException ce) { logger.error("Error tagging the resource: " + ce.getCause().getMessage()); - return; + throw ce; } waitForInputToContinue(scanner); @@ -291,7 +379,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and """); - logger.info("Do you want to delete the resources, including the workflow?"); + logger.info("Do you want to delete the resources, including the workflow? (y/n)"); String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { try { @@ -374,10 +462,11 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota // Check workflow status every 60 seconds. if (secondsElapsed % 60 == 0 || remainingTime <= 0) { - if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { + GetMatchingJobResponse response = actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join(); + if (response != null && "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status()))) { logger.info(""); // Move to the next line after countdown. - logger.info("Countdown complete: Workflow is in SUCCEEDED state!"); - break; + logger.info("Countdown complete: Workflow is in Completed state!"); + break; // Break out of the loop if the status is "SUCCEEDED" } } From 56d2f9f9588c6b558f3ad10d6c42fc41fcb7739c Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 27 Feb 2025 09:28:35 -0500 Subject: [PATCH 080/144] updated the service level readme --- .../example_code/entityresolution/README.md | 20 +-- .../entity/scenario/EntityResActions.java | 14 -- .../entity/scenario/EntityResScenario.java | 137 +++++++++--------- .../basics/entity_resolution/SPECIFICATION.md | 3 +- 4 files changed, 77 insertions(+), 97 deletions(-) diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md index 5ea507e27b2..d0a7195d8a5 100644 --- a/javav2/example_code/entityresolution/README.md +++ b/javav2/example_code/entityresolution/README.md @@ -45,16 +45,16 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. -- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L305) -- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L333) -- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L183) -- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) -- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) -- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L250) -- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L223) -- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L136) -- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L276) -- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L413) +- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L377) +- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L415) +- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L216) +- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L182) +- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L123) +- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L303) +- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L266) +- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) +- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L340) +- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L502) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 04f84f66971..09f55e8e8c4 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -321,7 +321,6 @@ public CompletableFuture getMatchingJobAsync(String jobI logger.info("Job status: " + response.status()); logger.info("Job details: " + response.toString()); } else { - // Handle the case where there is an exception. if (exception == null) { throw new CompletionException("An unknown error occurred while fetching the matching job.", null); } @@ -358,7 +357,6 @@ public CompletableFuture startMatchingJobAsync(String workflowName) { String jobId = response.jobId(); logger.info("Job ID: " + jobId); } else { - // Ensure exception is not null before accessing its cause. if (exception == null) { throw new CompletionException("An unknown error occurred while starting the job.", null); } @@ -531,8 +529,6 @@ public CompletableFuture tagEntityResource(String schemaMap if (cause instanceof ResourceNotFoundException) { throw new CompletionException("The resource to tag was not found.", cause); } - - // Wrap other AWS exceptions in a CompletionException. throw new CompletionException("Failed to tag the resource: " + exception.getMessage(), exception); } }); @@ -549,19 +545,9 @@ public CompletableFuture getJobInfo(String workflowName, String jobI logger.info("Job metrics fetched successfully for jobId: " + jobId); } else { Throwable cause = exception.getCause(); - - if (cause instanceof ResourceNotFoundException) { - // Handle validation errors if needed throw new CompletionException("Invalid request: Job id was not found.", cause); } - - if (cause instanceof ConflictException) { - // Handle conflict errors if needed - throw new CompletionException("A conflicting request occurred. Resolve conflicts before proceeding.", cause); - } - - // Generic failure case throw new CompletionException("Failed to fetch job info: " + exception.getMessage(), exception); } }) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 5a11e7d4529..0fca53b9663 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -44,6 +44,7 @@ public class EntityResScenario { private static Scanner scanner = new Scanner(System.in); private static EntityResActions actions = new EntityResActions(); + public static void main(String[] args) throws InterruptedException { logger.info("Welcome to the AWS Entity Resolution Scenario."); @@ -105,6 +106,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.error("An exception happened: " + (cause != null ? cause.getMessage() : ce.getMessage())); } } + private static void runScenario() throws InterruptedException { /* This JSON is a valid input for the AWS Entity Resolution service. @@ -136,18 +138,12 @@ private static void runScenario() throws InterruptedException { if (cause == null) { logger.error("Failed to upload input data: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ResourceNotFoundException) { - logger.error("The S3 bucket could not be found: {}", cause.getMessage(), cause); - } else { - logger.error("Failed to upload input data: {}", cause.getMessage(), cause); + logger.error("Failed to upload input data as the resource was not found: {}", cause.getMessage(), cause); } - - // Always wrap checked exceptions in a CompletionException - throw (cause instanceof RuntimeException) ? (RuntimeException) cause - : new CompletionException("Unhandled checked exception during input data upload", cause); + return; } logger.info("The JSON and CSV objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); @@ -174,7 +170,6 @@ private static void runScenario() throws InterruptedException { if (cause == null) { logger.error("Failed to create JSON schema mapping: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ConflictException) { @@ -182,10 +177,7 @@ private static void runScenario() throws InterruptedException { } else { logger.error("Unexpected error while creating schema mapping: {}", cause.getMessage(), cause); } - - // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException - throw (cause instanceof RuntimeException) ? (RuntimeException) cause - : new CompletionException("Unhandled checked exception in schema mapping creation", cause); + return; } try { @@ -194,10 +186,8 @@ private static void runScenario() throws InterruptedException { logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - if (cause == null) { logger.error("Failed to create CSV schema mapping: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ConflictException) { @@ -205,10 +195,7 @@ private static void runScenario() throws InterruptedException { } else { logger.error("Unexpected error while creating CSV schema mapping: {}", cause.getMessage(), cause); } - - // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException - throw (cause instanceof RuntimeException) ? (RuntimeException) cause - : new CompletionException("Unhandled checked exception in schema mapping creation", cause); + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -228,15 +215,16 @@ private static void runScenario() throws InterruptedException { """); waitForInputToContinue(scanner); try { - String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn, jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + String workflowArn = actions.createMatchingWorkflowAsync( + roleARN, workflowName, glueBucketName, jsonGlueTableArn, + jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); if (cause == null) { - // Log the exception and rethrow the CompletionException if no cause is found logger.error("An unexpected error occurred: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ValidationException) { @@ -244,18 +232,12 @@ private static void runScenario() throws InterruptedException { } else if (cause instanceof ConflictException) { logger.error("Workflow conflict detected: {}", cause.getMessage(), cause); } else { - logger.error("Unexpected error: {}", ce.getMessage(), ce); - } - - // Rethrow as a RuntimeException if cause is a RuntimeException, else rethrow CompletionException - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else { - throw new CompletionException("Unhandled exception occurred during workflow creation", cause); + logger.error("Unexpected error: {}", cause.getMessage(), cause); } + return; } - waitForInputToContinue(scanner); + waitForInputToContinue(scanner); logger.info(DASHES); logger.info("3. Start the matching job of the " + workflowName + " workflow."); waitForInputToContinue(scanner); @@ -265,18 +247,12 @@ private static void runScenario() throws InterruptedException { logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - if (cause instanceof ConflictException) { logger.error("Job conflict detected: {}", cause.getMessage(), cause); - } else if (cause instanceof RuntimeException) { - // Log and rethrow RuntimeException, ensuring it's captured in the first block - logger.error("Runtime error while starting the job: {}", cause.getMessage(), cause); - throw (RuntimeException) cause; // Rethrow RuntimeException for the first block to handle } else { logger.error("Unexpected error while starting the job: {}", ce.getMessage(), ce); - // For other checked exceptions, wrap them in a new CompletionException - throw new CompletionException("Unhandled checked exception while starting the job.", cause); } + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -288,8 +264,12 @@ private static void runScenario() throws InterruptedException { actions.getMatchingJobAsync(jobId, workflowName).join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); - throw ce; + if (cause instanceof ResourceNotFoundException) { + logger.error("The matching job not found: {}", cause.getMessage(), cause); + } else { + logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + return; } logger.info(DASHES); @@ -301,8 +281,13 @@ private static void runScenario() throws InterruptedException { jsonSchemaMappingArn = response.schemaArn(); logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { - logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); - throw ce; + Throwable cause = ce.getCause(); + if (cause instanceof ResourceNotFoundException) { + logger.error("Schema mapping not found: {}", cause.getMessage(), cause); + } else { + logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); + } + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -312,8 +297,8 @@ private static void runScenario() throws InterruptedException { try { actions.ListSchemaMappings(); } catch (CompletionException ce) { - logger.error("Error retrieving schema mapping: " + ce.getCause().getMessage()); - throw ce; + logger.error("Error retrieving schema mappings: " + ce.getCause().getMessage()); + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -331,7 +316,7 @@ private static void runScenario() throws InterruptedException { actions.tagEntityResource(jsonSchemaMappingArn).join(); } catch (CompletionException ce) { logger.error("Error tagging the resource: " + ce.getCause().getMessage()); - throw ce; + return; } waitForInputToContinue(scanner); @@ -352,32 +337,42 @@ private static void runScenario() throws InterruptedException { if (viewAns.equalsIgnoreCase("y")) { logger.info("You selected to view the Entity Resolution Workflow results."); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); - JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); - logger.info("Number of input records: {}", metrics.inputRecords()); - logger.info("Number of match ids: {}", metrics.matchIDs()); - logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); - logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); - logger.info(""" - - The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: - - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - - Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; - For example 'Bob Smith Jr.' compared to 'Bob Smith'. - The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, - the confidence level is lower for the differing email addresses. - - """); + try { + JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); + logger.info("Number of input records: {}", metrics.inputRecords()); + logger.info("Number of match ids: {}", metrics.matchIDs()); + logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); + logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); + logger.info(""" + + The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + + Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; + For example 'Bob Smith Jr.' compared to 'Bob Smith'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, + the confidence level is lower for the differing email addresses. + + """); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof ResourceNotFoundException) { + logger.error("The job not found: {}", cause.getMessage(), cause); + } else { + logger.error("Error retrieving job information: " + ce.getCause().getMessage()); + } + return; + } logger.info("Do you want to delete the resources, including the workflow? (y/n)"); String delAns = scanner.nextLine().trim(); diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 22bb26e21de..f65ed408c12 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -38,8 +38,7 @@ The AWS Entity Resolution Basics scenario executes the following operations. should be resolved and matched. The method `createMatchingWorkflow` is called. - Exception Handling: Check to see if a `ConflictException` is thrown, which - is thrown if the matching workflow already exists. If so, display the - message and end the program. + is thrown if the matching workflow already exists. ALso check to see if a `ValidationException` is thrown. If so, display the message and end the program. 3. **Start Matching Workflow**: - Description: Initiates a matching workflow by calling the From 7f7a0f12941ea95b3cf5aec3a6a0b9ca65ce9ae0 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:11:01 -0600 Subject: [PATCH 081/144] .NET v3: Default to on-demand tables in .NET DynamoDB examples. (#7263) * Updates to change to on-demand tables in .NET DynamoDB examples. --- dotnetv3/DotNetV3Examples.sln | 40 ++++++ .../CreateTablesLoadData.cs | 116 ++++++++---------- .../CreateTablesLoadDataExample.csproj | 4 +- dotnetv3/dynamodb/README.md | 22 ++-- .../LowLevelTableExample.cs | 20 +-- .../DynamoDB_Actions/DynamoDbMethods.cs | 6 +- .../DynamoDBMethods.cs | 6 +- .../PartiQL_Basics_Scenario/PartiQLBasics.cs | 6 +- .../PartiQL_Batch_Scenario/DynamoDBMethods.cs | 6 +- .../PartiQL_Batch_Scenario/PartiQLBatch.cs | 2 +- .../PartiQLBatchMethods.cs | 7 +- 11 files changed, 119 insertions(+), 116 deletions(-) diff --git a/dotnetv3/DotNetV3Examples.sln b/dotnetv3/DotNetV3Examples.sln index c6f351d608b..908a808da58 100644 --- a/dotnetv3/DotNetV3Examples.sln +++ b/dotnetv3/DotNetV3Examples.sln @@ -839,6 +839,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S3ObjectLockTests", "S3\sce EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConverseToolScenario", "Bedrock-runtime\Scenarios\ConverseToolScenario\ConverseToolScenario.csproj", "{83ED7BBE-5C9A-47AC-805B-351270069570}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DynamoDB_Actions", "DynamoDB_Actions", "{72466F30-810F-4963-B748-5154A6C49926}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDB_Actions", "dynamodb\scenarios\DynamoDB_Basics\DynamoDB_Actions\DynamoDB_Actions.csproj", "{B8843CE1-23AF-4E54-A916-C3FD94B4FF9A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNova", "AmazonNova", "{9FB5136B-F426-454C-B32D-855E07DBC0FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNovaText", "AmazonNovaText", "{6EA5F10D-C016-4AB0-B551-099DBFD74F95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConverseStream", "Bedrock-runtime\Models\AmazonNova\AmazonNovaText\ConverseStream\ConverseStream.csproj", "{C0AC14E2-54E9-426E-8A4A-7B64946A4715}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converse", "Bedrock-runtime\Models\AmazonNova\AmazonNovaText\Converse\Converse.csproj", "{FD901D0E-B970-42A3-B6E2-219BDA882F19}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AmazonNovaCanvas", "AmazonNovaCanvas", "{CDA2FA21-36E1-4847-A5A8-AF921C4BBBD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvokeModel", "Bedrock-runtime\Models\AmazonNova\AmazonNovaCanvas\InvokeModel\InvokeModel.csproj", "{1D2CF12A-F46E-4293-ABB3-2FD70D84328F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1921,6 +1937,22 @@ Global {83ED7BBE-5C9A-47AC-805B-351270069570}.Debug|Any CPU.Build.0 = Debug|Any CPU {83ED7BBE-5C9A-47AC-805B-351270069570}.Release|Any CPU.ActiveCfg = Release|Any CPU {83ED7BBE-5C9A-47AC-805B-351270069570}.Release|Any CPU.Build.0 = Release|Any CPU + {B8843CE1-23AF-4E54-A916-C3FD94B4FF9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8843CE1-23AF-4E54-A916-C3FD94B4FF9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8843CE1-23AF-4E54-A916-C3FD94B4FF9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8843CE1-23AF-4E54-A916-C3FD94B4FF9A}.Release|Any CPU.Build.0 = Release|Any CPU + {C0AC14E2-54E9-426E-8A4A-7B64946A4715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0AC14E2-54E9-426E-8A4A-7B64946A4715}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0AC14E2-54E9-426E-8A4A-7B64946A4715}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0AC14E2-54E9-426E-8A4A-7B64946A4715}.Release|Any CPU.Build.0 = Release|Any CPU + {FD901D0E-B970-42A3-B6E2-219BDA882F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD901D0E-B970-42A3-B6E2-219BDA882F19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD901D0E-B970-42A3-B6E2-219BDA882F19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD901D0E-B970-42A3-B6E2-219BDA882F19}.Release|Any CPU.Build.0 = Release|Any CPU + {1D2CF12A-F46E-4293-ABB3-2FD70D84328F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D2CF12A-F46E-4293-ABB3-2FD70D84328F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D2CF12A-F46E-4293-ABB3-2FD70D84328F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D2CF12A-F46E-4293-ABB3-2FD70D84328F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2303,6 +2335,14 @@ Global {93588ED1-A248-4F6C-85A4-27E9E65D8AC7} = {7EC94891-9A5F-47EF-9C97-8A280754525C} {BCCFBED0-E800-46C5-975B-7D404486F00F} = {7EC94891-9A5F-47EF-9C97-8A280754525C} {83ED7BBE-5C9A-47AC-805B-351270069570} = {BA23BB28-EC63-4330-8CA7-DEB1B6489580} + {72466F30-810F-4963-B748-5154A6C49926} = {3F9C4507-5BD7-4AA5-9EE0-538DE08FAF43} + {B8843CE1-23AF-4E54-A916-C3FD94B4FF9A} = {72466F30-810F-4963-B748-5154A6C49926} + {9FB5136B-F426-454C-B32D-855E07DBC0FE} = {6520EB28-F7B4-4581-B3D8-A06E9303B16B} + {6EA5F10D-C016-4AB0-B551-099DBFD74F95} = {9FB5136B-F426-454C-B32D-855E07DBC0FE} + {C0AC14E2-54E9-426E-8A4A-7B64946A4715} = {6EA5F10D-C016-4AB0-B551-099DBFD74F95} + {FD901D0E-B970-42A3-B6E2-219BDA882F19} = {6EA5F10D-C016-4AB0-B551-099DBFD74F95} + {CDA2FA21-36E1-4847-A5A8-AF921C4BBBD7} = {9FB5136B-F426-454C-B32D-855E07DBC0FE} + {1D2CF12A-F46E-4293-ABB3-2FD70D84328F} = {CDA2FA21-36E1-4847-A5A8-AF921C4BBBD7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} diff --git a/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadData.cs b/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadData.cs index 4c822237176..c1db8de3c26 100644 --- a/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadData.cs +++ b/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadData.cs @@ -71,26 +71,22 @@ public static async Task CreateTableProductCatalog(IAmazo { TableName = tableName, AttributeDefinitions = new List() - { - new AttributeDefinition - { - AttributeName = "Id", - AttributeType = ScalarAttributeType.N, - }, - }, + { + new AttributeDefinition + { + AttributeName = "Id", + AttributeType = ScalarAttributeType.N, + }, + }, KeySchema = new List() - { - new KeySchemaElement - { - AttributeName = "Id", - KeyType = KeyType.HASH, - }, - }, - ProvisionedThroughput = new ProvisionedThroughput + { + new KeySchemaElement { - ReadCapacityUnits = 10, - WriteCapacityUnits = 5, + AttributeName = "Id", + KeyType = KeyType.HASH, }, + }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); var result = await WaitTillTableCreated(client, tableName, response); @@ -112,26 +108,22 @@ public static async Task CreateTableForum(IAmazonDynamoDB { TableName = tableName, AttributeDefinitions = new List() - { - new AttributeDefinition - { - AttributeName = "Name", - AttributeType = ScalarAttributeType.S, - }, - }, + { + new AttributeDefinition + { + AttributeName = "Name", + AttributeType = ScalarAttributeType.S, + }, + }, KeySchema = new List() - { - new KeySchemaElement - { - AttributeName = "Name", - KeyType = KeyType.HASH, - }, - }, - ProvisionedThroughput = new ProvisionedThroughput + { + new KeySchemaElement { - ReadCapacityUnits = 10, - WriteCapacityUnits = 5, + AttributeName = "Name", + KeyType = KeyType.HASH, }, + }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); var result = await WaitTillTableCreated(client, tableName, response); @@ -154,36 +146,32 @@ public static async Task CreateTableThread(IAmazonDynamoD { TableName = tableName, AttributeDefinitions = new List() - { - new AttributeDefinition - { - AttributeName = "ForumName", // Hash attribute. - AttributeType = ScalarAttributeType.S, - }, - new AttributeDefinition - { - AttributeName = "Subject", - AttributeType = ScalarAttributeType.S, - }, - }, + { + new AttributeDefinition + { + AttributeName = "ForumName", // Hash attribute. + AttributeType = ScalarAttributeType.S, + }, + new AttributeDefinition + { + AttributeName = "Subject", + AttributeType = ScalarAttributeType.S, + }, + }, KeySchema = new List() - { - new KeySchemaElement - { - AttributeName = "ForumName", // Hash attribute - KeyType = KeyType.HASH, - }, - new KeySchemaElement - { - AttributeName = "Subject", // Range attribute - KeyType = KeyType.RANGE, - }, - }, - ProvisionedThroughput = new ProvisionedThroughput + { + new KeySchemaElement + { + AttributeName = "ForumName", // Hash attribute + KeyType = KeyType.HASH, + }, + new KeySchemaElement { - ReadCapacityUnits = 10, - WriteCapacityUnits = 5, + AttributeName = "Subject", // Range attribute + KeyType = KeyType.RANGE, }, + }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); var result = await WaitTillTableCreated(client, tableName, response); @@ -256,11 +244,7 @@ public static async Task CreateTableReply(IAmazonDynamoDB }, }, }, - ProvisionedThroughput = new ProvisionedThroughput - { - ReadCapacityUnits = 10, - WriteCapacityUnits = 5, - }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); var result = await WaitTillTableCreated(client, tableName, response); diff --git a/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadDataExample.csproj b/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadDataExample.csproj index a6f7af21bee..06fcb8c73d3 100644 --- a/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadDataExample.csproj +++ b/dotnetv3/dynamodb/CreateTablesLoadDataExample/CreateTablesLoadDataExample/CreateTablesLoadDataExample.csproj @@ -6,8 +6,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/dotnetv3/dynamodb/README.md b/dotnetv3/dynamodb/README.md index 407ad563340..0f18a83e129 100644 --- a/dotnetv3/dynamodb/README.md +++ b/dotnetv3/dynamodb/README.md @@ -47,18 +47,18 @@ Code excerpts that show you how to call individual service functions. - [BatchExecuteStatement](scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatchMethods.cs#L10) - [BatchGetItem](low-level-api/LowLevelBatchGet/LowLevelBatchGet.cs#L4) -- [BatchWriteItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L202) +- [BatchWriteItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L198) - [CreateTable](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L14) -- [DeleteItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L262) -- [DeleteTable](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L391) -- [DescribeTable](low-level-api/LowLevelTableExample/LowLevelTableExample.cs#L126) +- [DeleteItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L258) +- [DeleteTable](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L387) +- [DescribeTable](low-level-api/LowLevelTableExample/LowLevelTableExample.cs#L120) - [ExecuteStatement](scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/PartiQLMethods.cs#L163) -- [GetItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L171) -- [ListTables](low-level-api/LowLevelTableExample/LowLevelTableExample.cs#L102) -- [PutItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L89) -- [Query](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L297) -- [Scan](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L350) -- [UpdateItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L119) +- [GetItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L167) +- [ListTables](low-level-api/LowLevelTableExample/LowLevelTableExample.cs#L96) +- [PutItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L85) +- [Query](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L293) +- [Scan](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L346) +- [UpdateItem](scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs#L115) ### Scenarios @@ -240,4 +240,4 @@ in the `dotnetv3` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv3/dynamodb/low-level-api/LowLevelTableExample/LowLevelTableExample.cs b/dotnetv3/dynamodb/low-level-api/LowLevelTableExample/LowLevelTableExample.cs index 39254ea9468..3a7de3b81b7 100644 --- a/dotnetv3/dynamodb/low-level-api/LowLevelTableExample/LowLevelTableExample.cs +++ b/dotnetv3/dynamodb/low-level-api/LowLevelTableExample/LowLevelTableExample.cs @@ -80,18 +80,12 @@ private static async Task CreateExampleTable() KeyType = KeyType.RANGE //Sort key } }, - ProvisionedThroughput = new ProvisionedThroughput - { - ReadCapacityUnits = 5, - WriteCapacityUnits = 6 - }, - TableName = ExampleTableName + TableName = ExampleTableName, + BillingMode = BillingMode.PAY_PER_REQUEST, }); var tableDescription = response.TableDescription; - Console.WriteLine($"{tableDescription.TableName}: {tableDescription.TableStatus} \t " + - $"ReadsPerSec: {tableDescription.ProvisionedThroughput.ReadCapacityUnits} \t " + - $"WritesPerSec: {tableDescription.ProvisionedThroughput.WriteCapacityUnits}"); + Console.WriteLine($"{tableDescription.TableName}: {tableDescription.TableStatus}"); Console.WriteLine($"{ExampleTableName} - {tableDescription.TableStatus}"); @@ -136,21 +130,19 @@ private static async Task GetTableInformation() var table = response.Table; Console.WriteLine($"Name: {table.TableName}"); Console.WriteLine($"# of items: {table.ItemCount}"); - Console.WriteLine($"Provision Throughput (reads/sec): " + - $"{table.ProvisionedThroughput.ReadCapacityUnits}"); - Console.WriteLine($"Provision Throughput (writes/sec): " + - $"{table.ProvisionedThroughput.WriteCapacityUnits}"); + } // snippet-end:[dynamodb.dotnetv3.DescribeTableExample] // snippet-start:[dynamodb.dotnetv3.UpdateExampleTable] private static async Task UpdateExampleTable() { - Console.WriteLine("\n*** Updating table ***"); + Console.WriteLine("\n*** Updating table billing mode ***"); await Client.UpdateTableAsync(new UpdateTableRequest { TableName = ExampleTableName, + BillingMode = BillingMode.PROVISIONED, ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 6, diff --git a/dotnetv3/dynamodb/scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs b/dotnetv3/dynamodb/scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs index 99334150307..a2ec50ad485 100644 --- a/dotnetv3/dynamodb/scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs +++ b/dotnetv3/dynamodb/scenarios/DynamoDB_Basics/DynamoDB_Actions/DynamoDbMethods.cs @@ -51,11 +51,7 @@ public static async Task CreateMovieTableAsync(AmazonDynamoDBClient client KeyType = KeyType.RANGE, }, }, - ProvisionedThroughput = new ProvisionedThroughput - { - ReadCapacityUnits = 5, - WriteCapacityUnits = 5, - }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); // Wait until the table is ACTIVE and then report success. diff --git a/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/DynamoDBMethods.cs b/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/DynamoDBMethods.cs index aeb7a4562e2..fbfb7a52799 100644 --- a/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/DynamoDBMethods.cs +++ b/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/DynamoDBMethods.cs @@ -49,11 +49,7 @@ public static async Task CreateMovieTableAsync(string tableName) KeyType = KeyType.RANGE, }, }, - ProvisionedThroughput = new ProvisionedThroughput - { - ReadCapacityUnits = 5, - WriteCapacityUnits = 5, - }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); // Wait until the table is ACTIVE and then report success. diff --git a/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/PartiQLBasics.cs b/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/PartiQLBasics.cs index c06c822ca2e..4568b22ab86 100644 --- a/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/PartiQLBasics.cs +++ b/dotnetv3/dynamodb/scenarios/PartiQL_Basics_Scenario/PartiQL_Basics_Scenario/PartiQLBasics.cs @@ -3,14 +3,10 @@ // snippet-start:[PartiQL.dotnetv3.PartiQLBasicsScenario] -// Before you run this example, download 'movies.json' from -// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Js.02.html, -// and put it in the same folder as the example. - // Separator for the console display. var SepBar = new string('-', 80); const string tableName = "movie_table"; -const string movieFileName = "moviedata.json"; +const string movieFileName = @"..\..\..\..\..\..\..\..\resources\sample_files\movies.json"; var client = new AmazonDynamoDBClient(); diff --git a/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/DynamoDBMethods.cs b/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/DynamoDBMethods.cs index f674bc0bc7a..86878a0ea36 100644 --- a/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/DynamoDBMethods.cs +++ b/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/DynamoDBMethods.cs @@ -49,11 +49,7 @@ public static async Task CreateMovieTableAsync(string tableName) KeyType = KeyType.RANGE, }, }, - ProvisionedThroughput = new ProvisionedThroughput - { - ReadCapacityUnits = 5, - WriteCapacityUnits = 5, - }, + BillingMode = BillingMode.PAY_PER_REQUEST, }); // Wait until the table is ACTIVE and then report success. diff --git a/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatch.cs b/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatch.cs index 46c7325d57b..1cd917a8c5f 100644 --- a/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatch.cs +++ b/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatch.cs @@ -10,7 +10,7 @@ // Separator for the console display. var SepBar = new string('-', 80); const string tableName = "movie_table"; -const string movieFileName = "moviedata.json"; +const string movieFileName = @"..\..\..\..\..\..\..\..\resources\sample_files\movies.json"; DisplayInstructions(); diff --git a/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatchMethods.cs b/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatchMethods.cs index 6468cb86b83..c13d20c33ce 100644 --- a/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatchMethods.cs +++ b/dotnetv3/dynamodb/scenarios/PartiQL_Batch_Scenario/PartiQL_Batch_Scenario/PartiQLBatchMethods.cs @@ -120,7 +120,7 @@ public static async Task GetBatch( int year1, int year2) { - var getBatch = $"SELECT FROM {tableName} WHERE title = ? AND year = ?"; + var getBatch = $"SELECT * FROM {tableName} WHERE title = ? AND year = ?"; var statements = new List { new BatchStatementRequest @@ -153,7 +153,10 @@ public static async Task GetBatch( { response.Responses.ForEach(r => { - Console.WriteLine($"{r.Item["title"]}\t{r.Item["year"]}"); + if (r.Item.Any()) + { + Console.WriteLine($"{r.Item["title"]}\t{r.Item["year"]}"); + } }); return true; } From 8297618856d08bdf11fad09da7371be34e145609 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 09:56:10 -0500 Subject: [PATCH 082/144] rolled in review comments --- .../src/main/resources/TODO.md | 8 ------- .../com/myorg/EntityResolutionCdkApp.java | 24 +------------------ 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 javav2/example_code/entityresolution/src/main/resources/TODO.md diff --git a/javav2/example_code/entityresolution/src/main/resources/TODO.md b/javav2/example_code/entityresolution/src/main/resources/TODO.md deleted file mode 100644 index 8e3963dca2a..00000000000 --- a/javav2/example_code/entityresolution/src/main/resources/TODO.md +++ /dev/null @@ -1,8 +0,0 @@ -# Suggestions to improve the scenario - -Need to delete the schema mapping when you delete the workflow. - -Use two input data sources, since that is what a customer would do at a minimum. The input data for the scenario should contain records that do -and don't match. Make the second data source in CSV. - -When the job completes, display the results from the S3 bucket--both success and error. \ No newline at end of file diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java index e7428abe1da..ef25c7d2c34 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java @@ -14,29 +14,7 @@ public static void main(final String[] args) { App app = new App(); new EntityResolutionCdkStack(app, "EntityResolutionCdkStack", StackProps.builder() - // If you don't specify 'env', this stack will be environment-agnostic. - // Account/Region-dependent features and context lookups will not work, - // but a single synthesized template can be deployed anywhere. - - // Uncomment the next block to specialize this stack for the AWS Account - // and Region that are implied by the current CLI configuration. - /* - .env(Environment.builder() - .account(System.getenv("CDK_DEFAULT_ACCOUNT")) - .region(System.getenv("CDK_DEFAULT_REGION")) - .build()) - */ - - // Uncomment the next block if you know exactly what Account and Region you - // want to deploy the stack to. - /* - .env(Environment.builder() - .account("123456789012") - .region("us-east-1") - .build()) - */ - - // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html .build()); app.synth(); From 42ca9569fb7becbfd37cd855deeaff6d850dc5d5 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 10:14:57 -0500 Subject: [PATCH 083/144] rolled in review comments --- .../com/myorg/EntityResolutionCdkStack.java | 23 ------------------- .../basics/entity_resolution/SPECIFICATION.md | 12 ++++++++++ 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java index d46eeddf636..21127b7ae88 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -49,29 +49,6 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build(); // 3. Create a Glue table referencing the S3 bucket -/* CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") - .catalogId(this.getAccount()) - .databaseName(glueDatabase.getRef()) // Ensure Glue Table references the database correctly - .tableInput(CfnTable.TableInputProperty.builder() - .name("entity_resolution") // Fixed table name reference - .tableType("EXTERNAL_TABLE") - .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() - .columns(List.of( - CfnTable.ColumnProperty.builder().name("id").type("string").build(), // Fixed: id is a string, - CfnTable.ColumnProperty.builder().name("name").type("string").build(), - CfnTable.ColumnProperty.builder().name("email").type("string").build() - )) - .location("s3://" + glueDataBucket.getBucketName() + "/data/") // Append subpath for data - .inputFormat("org.apache.hadoop.mapred.TextInputFormat") - .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") - .serdeInfo(CfnTable.SerdeInfoProperty.builder() - .serializationLibrary("org.openx.data.jsonserde.JsonSerDe") // Set JSON SerDe - .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON - .build()) - .build()) - .build()) - .build();*/ - final CfnTable jsonErGlueTable = createGlueTable(jsonGlueTableName , jsonGlueTableName , glueDatabase.getRef() diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index f65ed408c12..ee64d45e962 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -108,6 +108,18 @@ With Entity Resolution, organizations can unlock the value of their data, improve decision-making, and enhance customer experiences by having a reliable, comprehensive view of their key entities. +Enter 'c' followed by to continue: +c + +To prepare the AWS resources needed for this scenario application, the next step uploads +a CloudFormation template whose resulting stack creates the following resources: + - An AWS Glue Data Catalog table + - An AWS IAM role + - An AWS S3 bucket + - An AWS Entity Resolution Schema + +It can take a couple minutes for the Stack to finish creating the resources. + Enter 'c' followed by to continue: c From be458f81b76f07da6fae3af6cda53c83e256e11e Mon Sep 17 00:00:00 2001 From: Laren-AWS <57545972+Laren-AWS@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:02:22 -0800 Subject: [PATCH 084/144] Test tools validator update. (#7266) Update to tools release 2025.08.0. --- .github/workflows/validate-doc-metadata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-doc-metadata.yml b/.github/workflows/validate-doc-metadata.yml index 28caa15e53a..c0396eb9c01 100644 --- a/.github/workflows/validate-doc-metadata.yml +++ b/.github/workflows/validate-doc-metadata.yml @@ -16,7 +16,7 @@ jobs: - name: checkout repo content uses: actions/checkout@v4 - name: validate metadata - uses: awsdocs/aws-doc-sdk-examples-tools@2025.07.0 + uses: awsdocs/aws-doc-sdk-examples-tools@2025.08.0 with: doc_gen_only: "False" strict_titles: "True" From bbb803c1d39ff77be1a13b786c014ca358b0ba7d Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 15:33:29 -0500 Subject: [PATCH 085/144] rolled in review comments --- .../entity/scenario/CloudFormationHelper.java | 2 +- .../entity/scenario/EntityResScenario.java | 156 +++++---- .../com/myorg/EntityResolutionCdkStack.java | 87 ++--- .../basics/entity_resolution/SPECIFICATION.md | 305 +++++++++++++----- 4 files changed, 360 insertions(+), 190 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java index 9de189ea437..12f48a586bd 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java @@ -100,7 +100,7 @@ public static void deployCloudFormationStack(String stackName) { } }).join(); } else { - logger.info("{} stack already exists", CFN_TEMPLATE); + logger.info("{} stack already exists", stackName); } } diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 0fca53b9663..0c2e9e58cc8 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -5,6 +5,7 @@ import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; import software.amazon.awssdk.services.entityresolution.model.AccessDeniedException; import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; @@ -17,6 +18,7 @@ import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; import software.amazon.awssdk.services.entityresolution.model.ThrottlingException; import software.amazon.awssdk.services.entityresolution.model.ValidationException; +import software.amazon.awssdk.services.s3.model.S3Exception; import java.util.Map; import java.util.Scanner; @@ -115,18 +117,18 @@ private static void runScenario() throws InterruptedException { Entity Resolution service. */ String json = """ - {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} - {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} - {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} + {"id":"1","name":"Jane Doe","email":"jane.doe@example.com"} + {"id":"2","name":"John Doe","email":"john.doe@example.com"} + {"id":"3","name":"Jorge Souza","email":"jorge_souza@example.com"} """; logger.info("Upload the following JSON objects to the {} S3 bucket.", glueBucketName); logger.info(json); String csv = """ id,name,email,phone - 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 - 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 - 3,Charlie Black,charlie.black@company.com,345-567-1234 - 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + 1,Jane B.,Doe,jane.doe@example.com,555-876-9846 + 2,John Doe Jr.,john.doe@example.com,555-654-3210 + 3,María García,maría_garcia@company.com,555-567-1234 + 4,Mary Major,mary_major@company.com,555-222-3333 """; logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); logger.info(csv); @@ -158,7 +160,7 @@ private static void runScenario() throws InterruptedException { and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - In this example, the schema mapping lines up with the fields in the JSON ans CSV objects. That is, + In this example, the schema mapping lines up with the fields in the JSON and CSV objects. That is, it contains these fields: id, name, and email. """); try { @@ -328,8 +330,8 @@ private static void runScenario() throws InterruptedException { You cannot view the result of the workflow that is in a running state. In order to view the results, you need to wait for the workflow that we started in step 3 to complete. - If you choose not to wait, you cannot view the results or delete the workflow. You would have to - perform both tasks manually in the AWS Management Console. + If you choose not to wait, you cannot view the results. You can perform + this task manually in the AWS Management Console. This can take up to 30 mins (y/n). """); @@ -343,27 +345,26 @@ private static void runScenario() throws InterruptedException { logger.info("Number of match ids: {}", metrics.matchIDs()); logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); + logger.info("The following represents the actual output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); logger.info(""" - - The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: - + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - - Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; - For example 'Bob Smith Jr.' compared to 'Bob Smith'. - The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, - the confidence level is lower for the differing email addresses. - + + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable Mary Major mary_major@company.com, 555-222-3333 4 ec05e7a55a0d4319b86da0a65286118f000040 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 María García maría_garcia@company.com 555-567-1234 3 201ed8241ec04f9aa7fcfd962220580500001369367187456 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 1 Jane Doe jane.doe@example.com 1 895c3a439dc44a298663d52c08635e1a0000434359738368 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 1 Jane B.Doe jane.doe@example.com 1 69c2b2190c60427c8f5a2daa7ce5d45b00001463856467968 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.8914204 2 John Doe john.doe@example.com 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s + arn:aws:glue:us-east-1::xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.8914204 2 John Doe Jr. john.doe@example.com 555-654-3210 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s + + Note that each of the last 2 records are considered a match even though the 'name' differs between the records; + For example 'John Doe Jr.' compared to 'John Doe'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. + """); + } catch (CompletionException ce) { Throwable cause = ce.getCause(); if (cause instanceof ResourceNotFoundException) { @@ -373,49 +374,64 @@ private static void runScenario() throws InterruptedException { } return; } + } - logger.info("Do you want to delete the resources, including the workflow? (y/n)"); - String delAns = scanner.nextLine().trim(); - if (delAns.equalsIgnoreCase("y")) { - try { - actions.deleteMatchingWorkflowAsync(workflowName).join(); - logger.info("Workflow deleted successfully!"); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; - } + waitForInputToContinue(scanner); + logger.info(DASHES); - try { - // Delete both schema mappings. - actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); - actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); - logger.info("Both schema mappings were deleted successfully!"); - } catch (RuntimeException e) { - logger.error("Error deleting schema mapping: {}", e.getMessage()); - return; - } + logger.info(DASHES); + logger.info("9. Do you want to delete the resources, including the workflow? (y/n)"); + logger.info(""" + You cannot delete the workflow that is in a running state. + In order to delete the workflow, you need to wait for the workflow to complete. + + You can delete the workflow manually in the AWS Management Console at a later time. + + If you already waited for the workflow to complete in the previous step, + the workflow is completed and you can delete it. + + If the workflow is not completed, this can take up to 30 mins (y/n). + """); + String delAns = scanner.nextLine().trim(); + if (delAns.equalsIgnoreCase("y")) { + try { + countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + actions.deleteMatchingWorkflowAsync(workflowName).join(); + logger.info("Workflow deleted successfully!"); + } catch (CompletionException ce) { + logger.info("Error deleting the workflow: {} ", ce.getMessage()); + return; + } - waitForInputToContinue(scanner); - logger.info(DASHES); - logger.info(""" - Now we delete the CloudFormation stack, which deletes - the resources that were created at the beginning - """); - waitForInputToContinue(scanner); - logger.info(DASHES); - try { - deleteResources(); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); - return; - } + try { + // Delete both schema mappings. + actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); + actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); + logger.info("Both schema mappings were deleted successfully!"); + } catch (CompletionException ce) { + logger.error("Error deleting schema mapping: {}", ce.getMessage()); + return; + } - } else { - logger.info("You can delete the Workflow later in the AWS Management console."); + waitForInputToContinue(scanner); + logger.info(DASHES); + logger.info(""" + Now we delete the CloudFormation stack, which deletes + the resources that were created at the beginning of this scenario. + """); + waitForInputToContinue(scanner); + logger.info(DASHES); + try { + deleteCloudFormationStack(); + } catch (RuntimeException e) { + logger.error("Failed to delete the stack: {}", e.getMessage()); + return; } + + } else { + logger.info("You can delete the AWS resources in the AWS Management Console."); } + waitForInputToContinue(scanner); logger.info(DASHES); @@ -472,10 +488,16 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } - private static void deleteResources() { - CloudFormationHelper.emptyS3Bucket(glueBucketName); - CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); - logger.info("Resources deleted successfully!"); + private static void deleteCloudFormationStack() { + try { + CloudFormationHelper.emptyS3Bucket(glueBucketName); + CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); + logger.info("Resources deleted successfully!"); + } catch (CloudFormationException e) { + throw new RuntimeException("Failed to delete CloudFormation stack: " + e.getMessage(), e); + } catch (S3Exception e) { + throw new RuntimeException("Failed to empty S3 bucket: " + e.getMessage(), e); + } } } // snippet-end:[entityres.java2_scenario.main] \ No newline at end of file diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java index 21127b7ae88..c8871872d69 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -35,10 +35,10 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St String uniqueId = UUID.randomUUID().toString().replace("-", ""); // Remove dashes to ensure compatibility Bucket erBucket = Bucket.Builder.create(this, "ErBucket") - .bucketName("erbucket" + uniqueId) - .versioned(false) - .removalPolicy(RemovalPolicy.DESTROY) - .build(); + .bucketName("erbucket" + uniqueId) + .versioned(false) + .removalPolicy(RemovalPolicy.DESTROY) + .build(); // 2. Create a Glue database CfnDatabase glueDatabase = CfnDatabase.Builder.create(this, "GlueDatabase") @@ -50,21 +50,21 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St // 3. Create a Glue table referencing the S3 bucket final CfnTable jsonErGlueTable = createGlueTable(jsonGlueTableName - , jsonGlueTableName - , glueDatabase.getRef() - , Map.of("id", "string", "name", "string", "email", "string") - , "s3://" + erBucket.getBucketName() + "/jsonData/" - , "org.openx.data.jsonserde.JsonSerDe"); + , jsonGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string") + , "s3://" + erBucket.getBucketName() + "/jsonData/" + , "org.openx.data.jsonserde.JsonSerDe"); // Ensure Glue Table is created after the Database jsonErGlueTable.addDependency(glueDatabase); final CfnTable csvErGlueTable = createGlueTable(csvGlueTableName - , csvGlueTableName - , glueDatabase.getRef() - , Map.of("id", "string", "name", "string", "email", "string", "phone", "string") - , "s3://" + erBucket.getBucketName() + "/csvData/" - , "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe"); + , csvGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string", "phone", "string") + , "s3://" + erBucket.getBucketName() + "/csvData/" + , "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe"); // Ensure Glue Table is created after the Database csvErGlueTable.addDependency(glueDatabase); @@ -100,9 +100,9 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build()); new CfnOutput(this, "CsvErGlueTableArn", CfnOutputProps.builder() - .value(createGlueTableArn(csvErGlueTable, csvGlueTableName)) - .description("The ARN of the CSV Glue Table") - .build()); + .value(createGlueTableArn(csvErGlueTable, csvGlueTableName)) + .description("The ARN of the CSV Glue Table") + .build()); new CfnOutput(this, "GlueDataBucketName", CfnOutputProps.builder() .value(erBucket.getBucketName()) // Outputs the bucket name @@ -110,41 +110,42 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build()); } - CfnTable createGlueTable(String id, String tableName, String databaseRef, Map schemaMap, String dataLocation, String serializationLib){ + CfnTable createGlueTable(String id, String tableName, String databaseRef, Map schemaMap, String dataLocation, String serializationLib) { return CfnTable.Builder.create(this, id) - .catalogId(this.getAccount()) - .databaseName(databaseRef) // Ensure Glue Table references the database correctly - .tableInput(CfnTable.TableInputProperty.builder() - .name(tableName) // Fixed table name reference - .tableType("EXTERNAL_TABLE") - .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() - .columns(createColumns(schemaMap)) - .location(dataLocation) // Append subpath for data - .inputFormat("org.apache.hadoop.mapred.TextInputFormat") - .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") - .serdeInfo(CfnTable.SerdeInfoProperty.builder() - .serializationLibrary(serializationLib) // Set JSON SerDe - .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON - .build()) - .build()) + .catalogId(this.getAccount()) + .databaseName(databaseRef) // Ensure Glue Table references the database correctly + .tableInput(CfnTable.TableInputProperty.builder() + .name(tableName) // Fixed table name reference + .tableType("EXTERNAL_TABLE") + .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() + .columns(createColumns(schemaMap)) + .location(dataLocation) // Append subpath for data + .inputFormat("org.apache.hadoop.mapred.TextInputFormat") + .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") + .serdeInfo(CfnTable.SerdeInfoProperty.builder() + .serializationLibrary(serializationLib) // Set JSON SerDe + .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON .build()) - .build(); + .build()) + .build()) + .build(); } + List createColumns(Map schemaMap) { return schemaMap.entrySet().stream() - .map(entry -> CfnTable.ColumnProperty.builder() - .name(entry.getKey()) - .type(entry.getValue()) - .build()) - .toList(); + .map(entry -> CfnTable.ColumnProperty.builder() + .name(entry.getKey()) + .type(entry.getValue()) + .build()) + .toList(); } String createGlueTableArn(CfnTable glueTable, String glueTableName) { return String.format("arn:aws:glue:%s:%s:table/%s/%s" - , this.getRegion() - , this.getAccount() - , glueTable.getDatabaseName() - , glueTableName + , this.getRegion() + , this.getAccount() + , glueTable.getDatabaseName() + , glueTableName ); } } diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index ee64d45e962..9725de70998 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -55,36 +55,45 @@ The AWS Entity Resolution Basics scenario executes the following operations. thrown, which indicates that the workflow cannot be found. If the exception is thrown, display the message and end the program. -5. **List Matching Workflows**: - - Description: Lists all matching workflows created within the account by - calling the `listMatchingWorkflows` method. - - Exception Handling: Check to see if an `CompletionException` is thrown. If - so, display the message and end the program. - -6. **Get Schema Mapping**: +5. **Get Schema Mapping**: - Description: Returns the `SchemaMapping` of a given name by calling the `getSchemaMapping` method. - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. +6. **List Matching Workflows**: + - Description: Lists all matching workflows created within the account by + calling the `listMatchingWorkflows` method. + - Exception Handling: Check to see if an `CompletionException` is thrown. If + so, display the message and end the program. + 7. **Tag Resource**: - Description: Adds tags associated with an AWS Entity Resolution resource by calling the`tagResource` method. - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program -8. **Delete Matching Workflow**: - - Description: Deletes a specified matching workflow by calling the - `deleteMatchingWorkflow` method. - - Exception Handling: Check to see if an `ConflictException` is thrown. If +8. **View the results of the AWS Entity Resolution Workflow**: + - Description: View the workflow results by calling the + `getMatchingJob` method. + - Exception Handling: Check to see if an `ResourceNotFoundException` is thrown. If so, display the message and end the program. +9. **Delete the AWS resources**: + - Description: Delete the AWS resouces including the workflow and schema mappings by calling the + `deleteMatchingWorkflow` and `deleteSchemaMapping` methods. + - Exception Handling: Check to see if an `ResourceNotFoundException` is thrown. If + so, display the message and end the program. + - Finally delete the CloudFormation Stack by calling these method: + - CloudFormationHelper.emptyS3Bucket(glueBucketName); + - CloudFormationHelper.destroyCloudFormationStack + ### Program execution The following shows the output of the AWS Entity Resolution Basics scenario in the console. ``` -Welcome to the AWS Entity Resolution Scenario. +Welcome to the AWS Entity Resolution Scenario. AWS Entity Resolution is a fully-managed machine learning service provided by Amazon Web Services (AWS) that helps organizations extract, link, and organize information from multiple data sources. It leverages natural @@ -108,16 +117,20 @@ With Entity Resolution, organizations can unlock the value of their data, improve decision-making, and enhance customer experiences by having a reliable, comprehensive view of their key entities. + Enter 'c' followed by to continue: c +Continuing with the program... +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- To prepare the AWS resources needed for this scenario application, the next step uploads a CloudFormation template whose resulting stack creates the following resources: - - An AWS Glue Data Catalog table - - An AWS IAM role - - An AWS S3 bucket - - An AWS Entity Resolution Schema - +- An AWS Glue Data Catalog table +- An AWS IAM role +- An AWS S3 bucket +- An AWS Entity Resolution Schema + It can take a couple minutes for the Stack to finish creating the resources. @@ -125,36 +138,33 @@ Enter 'c' followed by to continue: c Continuing with the program... +Generating resources... +Stack creation requested, ARN is arn:aws:cloudformation:us-east-1:814548047983:stack/EntityResolutionCdkStack/858988e0-f604-11ef-916b-0affc298c80f +Stack created successfully -------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -Upload the JSON to the glue-5ffb912c3d534e8493bac675c2a3196d S3 bucket if it does not exist -[ - { - "id": "1", - "name": "Alice Johnson", - "email": "alice.johnson@example.com" - }, - { - "id": "2", - "name": "Bob Smith", - "email": "bob.smith@example.com" - }, - { - "id": "3", - "name": "Charlie Black", - "email": "charlie.black@example.com" - } -] + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Upload the following JSON objects to the erbucketf684533d2680435fa99d24b1bdaf5179 S3 bucket. +{"id":"1","name":"Jane Doe","email":"jane.doe@example.com"} +{"id":"2","name":"John Doe","email":"john.doe@example.com"} +{"id":"3","name":"Jorge Souza","email":"jorge_souza@example.com"} + +Upload the following CSV data to the erbucketf684533d2680435fa99d24b1bdaf5179 S3 bucket. +id,name,email,phone +1,Jane B.,Doe,jane.doe@example.com,555-876-9846 +2,John Doe Jr.,john.doe@example.com,555-654-3210 +3,María García,maría_garcia@company.com,555-567-1234 +4,Mary Major,mary_major@company.com,555-222-3333 Enter 'c' followed by to continue: c Continuing with the program... -SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". -SLF4J: Defaulting to no-operation (NOP) logger implementation -SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. -The JSON exists in glue-5ffb912c3d534e8493bac675c2a3196d +The JSON and CSV objects have been uploaded to the S3 bucket. Enter 'c' followed by to continue: c @@ -163,21 +173,19 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- 1. Create Schema Mapping -Entity Resolution Schema Mapping aligns and integrates data from +Entity Resolution schema mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. -In this example, the schema mapping lines up with the fields in the JSON. That is, +In this example, the schema mapping lines up with the fields in the JSON and CSV objects. That is, it contains these fields: id, name, and email. - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Schema Mapping Created Successfully! +[jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2] schema mapping Created Successfully! +The JSON schema mapping name is jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 +[csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c] schema mapping Created Successfully! +The CSV schema mapping name is csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c Enter 'c' followed by to continue: c @@ -192,7 +200,9 @@ customers or products. Using techniques like schema mapping, data profiling, and machine learning algorithms, it evaluates attributes like names or emails to detect duplicates or relationships, even with variations or inconsistencies. -The workflow outputs consolidated, de-duplicated data, +The workflow outputs consolidated, de-duplicated data. + +We will use the machine learning-based matching technique. Enter 'c' followed by to continue: @@ -200,20 +210,20 @@ c Continuing with the program... Workflow created successfully. -The workflow ARN is: arn:aws:entityresolution:us-east-1:814548047983:matchingworkflow/MyMatchingWorkflow450 +The workflow ARN is: arn:aws:entityresolution:us-east-1:814548047983:matchingworkflow/workflow-39216b7f-f00b-4896-84ae-cd7edcfc7872 Enter 'c' followed by to continue: c Continuing with the program... -------------------------------------------------------------------------------- -3. Start the matching job of the MyMatchingWorkflow450 workflow. +3. Start the matching job of the workflow-39216b7f-f00b-4896-84ae-cd7edcfc7872 workflow. Enter 'c' followed by to continue: c Continuing with the program... -Job ID: ec2dbd1717624b2b806ed93a04c20049 +Job ID: f25d2707729646a4af27874d991e22c5 The matching job was successfully started. Enter 'c' followed by to continue: @@ -222,32 +232,70 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -4. Get details for job ec2dbd1717624b2b806ed93a04c20049 +4. While the matching job is running, let's look at other API methods. First, let's get details for job f25d2707729646a4af27874d991e22c5 Enter 'c' followed by to continue: c Continuing with the program... -Job status: QUEUED -Job details: GetMatchingJobResponse(JobId=ec2dbd1717624b2b806ed93a04c20049, StartTime=2025-01-30T18:37:57.475Z, Status=QUEUED) +Job status: RUNNING +Job details: GetMatchingJobResponse(JobId=f25d2707729646a4af27874d991e22c5, StartTime=2025-02-28T18:49:14.921Z, Status=RUNNING) -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -5. Get Schema Mapping. +5. Get the schema mapping for the JSON data. Enter 'c' followed by to continue: c Continuing with the program... Attribute Name: id, Attribute Type: UNIQUE_ID -Attribute Name: name, Attribute Type: STRING -Attribute Name: email, Attribute Type: STRING -Schema mapping retrieval completed. +Attribute Name: name, Attribute Type: NAME +Attribute Name: email, Attribute Type: EMAIL_ADDRESS +Schema mapping ARN is arn:aws:entityresolution:us-east-1:814548047983:schemamapping/jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 + +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- 6. List Schema Mappings. +Schema Mapping Name: csv-33f8e392-74e7-4a08-900a-652b94f86250 +Schema Mapping Name: csv-3b68e38b-1d5c-4836-bfc7-92ac7339e5c7 +Schema Mapping Name: csv-4f547deb-56c1-4923-9119-556bc43df08d +Schema Mapping Name: csv-6fe8bbc3-ebb5-4800-ab49-a89f75a87905 +Schema Mapping Name: csv-812ecad3-3175-49c3-93a5-d3175396d6e7 +Schema Mapping Name: csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c +Schema Mapping Name: csv-90a464e1-f050-422c-8f5f-0726541a5858 +Schema Mapping Name: csv-ebad3e3d-27be-4ed4-ae35-7401265e57bd +Schema Mapping Name: csv-f752d395-857b-4106-b2f2-85e1da5e3040 +Schema Mapping Name: jsonschema-363dc915-0540-406e-8d3f-4f1435e0b942 +Schema Mapping Name: jsonschema-5b1ad3e1-a840-4c4f-b791-5e9e1893fe7e +Schema Mapping Name: jsonschema-8623e0ec-bb8c-4fe2-a998-609eae08d84d +Schema Mapping Name: jsonschema-93d5fd04-f10e-4274-a181-489bea7b92db +Schema Mapping Name: jsonschema-b1653c13-ce77-471d-a3d5-ae4877216a74 +Schema Mapping Name: jsonschema-c09b3414-384c-4e3d-90c8-61e48abde04d +Schema Mapping Name: jsonschema-d9a6edc0-a9bd-4553-bb71-fbf0d6064ef9 +Schema Mapping Name: jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 +Schema Mapping Name: jsonschema-f0a259e0-f4e5-493a-bfd5-32740d2fa24d +Schema Mapping Name: schema2135 +Schema Mapping Name: schema435 +Schema Mapping Name: schema455 +Schema Mapping Name: schema456 +Schema Mapping Name: schema4648 +Schema Mapping Name: schema4720 +Schema Mapping Name: schema4848 +Schema Mapping Name: schema6758 +Schema Mapping Name: schema8775 +Schema Mapping Name: schemaName100 + +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -7. Tag the schema450resource. +7. Tag the jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 resource. Tags can help you organize and categorize your Entity Resolution resources. You can also use them to scope user permissions by granting a user permission to access or change only resources with certain tag values. @@ -256,14 +304,72 @@ the SchemaMapping is tagged. Successfully tagged the resource. +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -8. Delete the AWS Entity Resolution Workflow. -You cannot delete a workflow that is in a running state. -Would you like to wait for the workflow to complete. +8. View the results of the AWS Entity Resolution Workflow. +You cannot view the result of the workflow that is in a running state. +In order to view the results, you need to wait for the workflow that we started in step 3 to complete. + +If you choose not to wait, you cannot view the results. You can perform +this task manually in the AWS Management Console. + This can take up to 30 mins (y/n). -n +y +You selected to view the Entity Resolution Workflow results. +29:01Job status: RUNNING +28:01Job status: RUNNING +27:01Job status: RUNNING +26:01Job status: RUNNING +25:01Job status: RUNNING +24:01Job status: RUNNING +23:01Job status: RUNNING +22:01Job status: RUNNING +21:01Job status: RUNNING +20:01Job status: RUNNING +19:01Job status: RUNNING +18:01Job status: RUNNING +17:01Job status: RUNNING +16:01Job status: RUNNING +15:01Job status: RUNNING +14:01Job status: RUNNING +13:01Job status: RUNNING +12:01Job status: RUNNING +11:01Job status: RUNNING +10:01Job status: RUNNING +09:01Job status: RUNNING +08:01Job status: RUNNING +07:01Job status: SUCCEEDED + +Countdown complete: Workflow is in Completed state! +Job metrics fetched successfully for jobId: f25d2707729646a4af27874d991e22c5 +Number of input records: 7 +Number of match ids: 6 +Number of records not processed: 0 +Number of total records processed: 7 +The following explains the output data generated by the Entity Resolution workflow. The output data is stored in the erbucketf684533d2680435fa99d24b1bdaf5179 bucket. + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- --------------------------------------------------- + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- --------------------------------------------------- + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 + +Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; +For example 'Bob Smith Jr.' compared to 'Bob Smith'. +The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, +the confidence level is lower for the differing email addresses. + + Enter 'c' followed by to continue: c @@ -271,9 +377,50 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -This concludes the AWS Entity Resolution scenario. +9. Do you want to delete the resources, including the workflow? (y/n) +You cannot delete the workflow that is in a running state. +In order to delete the workflow, you need to wait for the workflow to complete. + +You can delete the workflow manually in the AWS Management Console at a later time. + +If you already waited for the workflow to complete in the previous step, +the workflow is completed and you can delete it. + +If the workflow is not completed, this can take up to 30 mins (y/n). + +y +workflow-39216b7f-f00b-4896-84ae-cd7edcfc7872 was deleted +Workflow deleted successfully! +Schema mapping 'jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2' deleted successfully. +Schema mapping 'csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c' deleted successfully. +Both schema mappings were deleted successfully! + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +Now we delete the CloudFormation stack, which deletes +the resources that were created at the beginning of this scenario. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- +Delete stack requested .... +Stack deleted successfully. +Resources deleted successfully! + +Enter 'c' followed by to continue: +c +Continuing with the program... +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +This concludes the AWS Entity Resolution scenario. +-------------------------------------------------------------------------------- ``` @@ -281,20 +428,20 @@ This concludes the AWS Entity Resolution scenario. The following table describes the metadata used in this Basics Scenario. -| action | metadata file | metadata key | -|-------------------------|------------------------|-------------------------------| -| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | -| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | -| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | -| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | -| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | -| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | -| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | -| `tagResource ` | entity_metadata.yaml | entity_TagResource | -| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | -| `deleteMapping ` | entity_metadata.yaml | entity_DeleteSchemaMapping | -| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | -| `scenario` | entity_metadata.yaml | entity_Scenario | +| action | metadata file | metadata key | +|------------------------|----------------------------------|--------------------------------------| +| `createWorkflow` | entityresolution_metadata.yaml |entityresolution_CreateWorkflow | +| `createSchemaMapping` | entityresolution_metadata.yaml |entityresolution_CreateMapping | +| `startMatchingJob` | entityresolution_metadata.yaml |entityresolution_StartMatchingJob | +| `getMatchingJob` | entityresolution_metadata.yaml |entityresolution_GetMatchingJob | +| `listMatchingWorkflows`| entityresolution_metadata.yaml |entityresolution_ListMatchingWorkflows| +| `getSchemaMapping` | entityresolution_metadata.yaml |entityresolution_GetSchemaMapping | +| `listSchemaMappings` | entityresolution_metadata.yaml |entityresolution_ListSchemaMappings | +| `tagResource ` | entityresolution_metadata.yaml |entityresolution_TagResource | +| `deleteWorkflow ` | entityresolution_metadata.yaml |entityresolution_DeleteWorkflow | +| `deleteMapping ` | entityresolution_metadata.yaml |entityresolution_DeleteSchemaMapping | +| `listMappingJobs ` | entityresolution_metadata.yaml |entityresolution_Hello | +| `scenario` | entityresolution_metadata.yaml |entityresolution_Scenario | From c1ec19b10f94ec9173ac40554958741ec4c25f83 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 16:31:28 -0500 Subject: [PATCH 086/144] rolled in review comments --- .../example/entity/scenario/EntityResScenario.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 0c2e9e58cc8..ea2e2a68a77 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -3,23 +3,17 @@ package com.example.entity.scenario; - -import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; -import software.amazon.awssdk.services.entityresolution.model.AccessDeniedException; import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.JobMetrics; import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; -import software.amazon.awssdk.services.entityresolution.model.ThrottlingException; import software.amazon.awssdk.services.entityresolution.model.ValidationException; import software.amazon.awssdk.services.s3.model.S3Exception; - import java.util.Map; import java.util.Scanner; import java.util.UUID; @@ -336,9 +330,11 @@ private static void runScenario() throws InterruptedException { This can take up to 30 mins (y/n). """); String viewAns = scanner.nextLine().trim(); + boolean isComplete = false; if (viewAns.equalsIgnoreCase("y")) { logger.info("You selected to view the Entity Resolution Workflow results."); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + isComplete = true; try { JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); logger.info("Number of input records: {}", metrics.inputRecords()); @@ -395,7 +391,9 @@ private static void runScenario() throws InterruptedException { String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { try { - countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + if (!isComplete) { + countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + } actions.deleteMatchingWorkflowAsync(workflowName).join(); logger.info("Workflow deleted successfully!"); } catch (CompletionException ce) { From 8d1ea3a7346c4140a35df6bc0898355e697dbb02 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 16:39:34 -0500 Subject: [PATCH 087/144] rolled in review comments --- .../basics/entity_resolution/SPECIFICATION.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 9725de70998..1f0880688f4 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -428,20 +428,20 @@ This concludes the AWS Entity Resolution scenario. The following table describes the metadata used in this Basics Scenario. -| action | metadata file | metadata key | -|------------------------|----------------------------------|--------------------------------------| -| `createWorkflow` | entityresolution_metadata.yaml |entityresolution_CreateWorkflow | -| `createSchemaMapping` | entityresolution_metadata.yaml |entityresolution_CreateMapping | -| `startMatchingJob` | entityresolution_metadata.yaml |entityresolution_StartMatchingJob | -| `getMatchingJob` | entityresolution_metadata.yaml |entityresolution_GetMatchingJob | -| `listMatchingWorkflows`| entityresolution_metadata.yaml |entityresolution_ListMatchingWorkflows| -| `getSchemaMapping` | entityresolution_metadata.yaml |entityresolution_GetSchemaMapping | -| `listSchemaMappings` | entityresolution_metadata.yaml |entityresolution_ListSchemaMappings | -| `tagResource ` | entityresolution_metadata.yaml |entityresolution_TagResource | -| `deleteWorkflow ` | entityresolution_metadata.yaml |entityresolution_DeleteWorkflow | -| `deleteMapping ` | entityresolution_metadata.yaml |entityresolution_DeleteSchemaMapping | -| `listMappingJobs ` | entityresolution_metadata.yaml |entityresolution_Hello | -| `scenario` | entityresolution_metadata.yaml |entityresolution_Scenario | +| action | metadata file | metadata key | +|------------------------|--------------------------------|--------------------------------------------| +| `createWorkflow` | entityresolution_metadata.yaml |entityresolution_CreateMatchingWorkflow | +| `createSchemaMapping` | entityresolution_metadata.yaml |entityresolution_CreateSchemaMapping | +| `startMatchingJob` | entityresolution_metadata.yaml |entityresolution_StartMatchingJob | +| `getMatchingJob` | entityresolution_metadata.yaml |entityresolution_GetMatchingJob | +| `listMatchingWorkflows`| entityresolution_metadata.yaml |entityresolution_ListMatchingWorkflows | +| `getSchemaMapping` | entityresolution_metadata.yaml |entityresolution_GetSchemaMapping | +| `listSchemaMappings` | entityresolution_metadata.yaml |entityresolution_ListSchemaMappings | +| `tagResource ` | entityresolution_metadata.yaml |entityresolution_TagEntityResource | +| `deleteWorkflow ` | entityresolution_metadata.yaml |ntityresolution_DeleteMatchingWorkflow | +| `deleteMapping ` | entityresolution_metadata.yaml |entityresolution_DeleteSchemaMapping | +| `listMappingJobs ` | entityresolution_metadata.yaml |entityresolution_Hello | +| `scenario` | entityresolution_metadata.yaml |entityresolution_Scenario | From 04d711d8087c352f15762e7c03ca6d7f36f4927e Mon Sep 17 00:00:00 2001 From: scmacdon Date: Sat, 1 Mar 2025 15:48:12 -0500 Subject: [PATCH 088/144] rolled in review comments --- .../java/com/example/entity/scenario/EntityResScenario.java | 2 +- .../entityresolution/src/main/resources/data.csv | 5 ----- .../entityresolution/src/main/resources/data.json | 3 --- 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 javav2/example_code/entityresolution/src/main/resources/data.csv delete mode 100644 javav2/example_code/entityresolution/src/main/resources/data.json diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index ea2e2a68a77..cb6067b1662 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -341,7 +341,7 @@ private static void runScenario() throws InterruptedException { logger.info("Number of match ids: {}", metrics.matchIDs()); logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); - logger.info("The following represents the actual output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); + logger.info("The following represents the output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); logger.info(""" ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s diff --git a/javav2/example_code/entityresolution/src/main/resources/data.csv b/javav2/example_code/entityresolution/src/main/resources/data.csv deleted file mode 100644 index 3ec062e335d..00000000000 --- a/javav2/example_code/entityresolution/src/main/resources/data.csv +++ /dev/null @@ -1,5 +0,0 @@ - id,name,email,phone - 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 - 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 - 3,Charlie Black,charlie.black@company.com,345-567-1234 - 7,Jane E. Doe,jane_doe@company.com,111-222-3333 \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/data.json b/javav2/example_code/entityresolution/src/main/resources/data.json deleted file mode 100644 index 0375ab4e2be..00000000000 --- a/javav2/example_code/entityresolution/src/main/resources/data.json +++ /dev/null @@ -1,3 +0,0 @@ -{"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} -{"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} -{"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} \ No newline at end of file From 87cc429d694d9915b844558ececef6f60828ed57 Mon Sep 17 00:00:00 2001 From: Matas Date: Mon, 3 Mar 2025 09:23:43 -0500 Subject: [PATCH 089/144] Kotlin: replace `CrtAwsSigner` with `DefaultAwsSigner` (#7262) Replace CrtAwsSigner with DefaultAwsSigner --- .../s3/src/main/kotlin/com/kotlin/s3/MrapExample.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kotlin/services/s3/src/main/kotlin/com/kotlin/s3/MrapExample.kt b/kotlin/services/s3/src/main/kotlin/com/kotlin/s3/MrapExample.kt index f432f64b523..0714df6f7d4 100644 --- a/kotlin/services/s3/src/main/kotlin/com/kotlin/s3/MrapExample.kt +++ b/kotlin/services/s3/src/main/kotlin/com/kotlin/s3/MrapExample.kt @@ -27,7 +27,7 @@ import aws.sdk.kotlin.services.s3control.model.Region import aws.sdk.kotlin.services.sts.StsClient import aws.sdk.kotlin.services.sts.getCallerIdentity import aws.sdk.kotlin.services.sts.model.GetCallerIdentityRequest -import aws.smithy.kotlin.runtime.auth.awssigning.crt.CrtAwsSigner +import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.content.decodeToString import aws.smithy.kotlin.runtime.http.auth.SigV4AsymmetricAuthScheme @@ -204,10 +204,10 @@ class MrapExample { companion object { // snippet-start:[s3.kotlin.mrap.create-s3client] suspend fun createS3Client(): S3Client { - // Configure your S3Client to use the Asymmetric Sigv4 (Sigv4a) signing algorithm. - val sigV4AScheme = SigV4AsymmetricAuthScheme(CrtAwsSigner) + // Configure your S3Client to use the Asymmetric SigV4 (SigV4a) signing algorithm. + val sigV4aScheme = SigV4AsymmetricAuthScheme(DefaultAwsSigner) val s3 = S3Client.fromEnvironment { - authSchemes = listOf(sigV4AScheme) + authSchemes = listOf(sigV4aScheme) } return s3 } From 60dcd5ddc37395a47c7a717bd1966e3a389fc00c Mon Sep 17 00:00:00 2001 From: Brian Murray <40031786+brmur@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:00:13 +0000 Subject: [PATCH 090/144] Dynamodb updates (#7267) * test * Update JS DynamoDB examples to use on-demand rather than provisioned * Update Rust and Ruby DynamoDB examples to use on-demand rather than provisioned * Update Rust and Ruby DynamoDB examples to use on-demand rather than provisioned * Update Rust and Ruby DynamoDB examples to use on-demand rather than provisioned * Update Rust and Ruby DynamoDB examples to use on-demand rather than provisioned --- .../dynamodb/actions/create-table.js | 5 +---- .../tests/delete-table.integration.test.js | 5 +---- ruby/Gemfile | 2 +- ruby/Gemfile.lock | 3 ++- ruby/example_code/dynamodb/README.md | 2 +- ruby/example_code/dynamodb/scaffold.rb | 2 +- .../integration/tests/update_label.rs | 11 +++-------- rustv1/examples/dynamodb/src/bin/crud.rs | 10 ++-------- .../dynamodb/src/bin/dynamodb-helloworld.rs | 10 ++-------- rustv1/examples/dynamodb/src/bin/partiql.rs | 12 +++--------- .../examples/dynamodb/src/scenario/create.rs | 12 +++--------- .../dynamodb/src/scenario/movies/startup.rs | 19 +++++-------------- 12 files changed, 25 insertions(+), 68 deletions(-) diff --git a/javascriptv3/example_code/dynamodb/actions/create-table.js b/javascriptv3/example_code/dynamodb/actions/create-table.js index 925c0103b02..9f2d22b698a 100644 --- a/javascriptv3/example_code/dynamodb/actions/create-table.js +++ b/javascriptv3/example_code/dynamodb/actions/create-table.js @@ -26,10 +26,7 @@ export const main = async () => { KeyType: "HASH", }, ], - ProvisionedThroughput: { - ReadCapacityUnits: 1, - WriteCapacityUnits: 1, - }, + BillingMode: "PAY_PER_REQUEST", }); const response = await client.send(command); diff --git a/javascriptv3/example_code/dynamodb/tests/delete-table.integration.test.js b/javascriptv3/example_code/dynamodb/tests/delete-table.integration.test.js index 8e0418081e2..1ca23a54e71 100644 --- a/javascriptv3/example_code/dynamodb/tests/delete-table.integration.test.js +++ b/javascriptv3/example_code/dynamodb/tests/delete-table.integration.test.js @@ -28,10 +28,7 @@ describe("delete-table", () => { KeyType: "HASH", }, ], - ProvisionedThroughput: { - ReadCapacityUnits: 1, - WriteCapacityUnits: 1, - }, + BillingMode: "PAY_PER_REQUEST", }); await client.send(createTableCommand); diff --git a/ruby/Gemfile b/ruby/Gemfile index 4cafb94cfc6..5dd220f262e 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -1,5 +1,5 @@ source 'https://rubygems.org' -ruby '3.1.2' +ruby '3.3.7' gem 'aws-sdk' gem 'cli-ui' diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 923f7fe9eef..f758ba18ef2 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -1520,6 +1520,7 @@ GEM PLATFORMS arm64-darwin-22 + x64-mingw-ucrt x86_64-linux DEPENDENCIES @@ -1545,7 +1546,7 @@ DEPENDENCIES zip RUBY VERSION - ruby 3.1.2p20 + ruby 3.3.7p123 BUNDLED WITH 2.3.7 diff --git a/ruby/example_code/dynamodb/README.md b/ruby/example_code/dynamodb/README.md index 4256dd73186..60304150f70 100644 --- a/ruby/example_code/dynamodb/README.md +++ b/ruby/example_code/dynamodb/README.md @@ -184,4 +184,4 @@ To learn more about the contributing process, see [CONTRIBUTING.md](../../../CON Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/ruby/example_code/dynamodb/scaffold.rb b/ruby/example_code/dynamodb/scaffold.rb index b36f241f080..748ce88353c 100644 --- a/ruby/example_code/dynamodb/scaffold.rb +++ b/ruby/example_code/dynamodb/scaffold.rb @@ -68,7 +68,7 @@ def create_table(table_name) { attribute_name: 'year', attribute_type: 'N' }, { attribute_name: 'title', attribute_type: 'S' } ], - provisioned_throughput: { read_capacity_units: 10, write_capacity_units: 10 } + billing_mode: 'PAY_PER_REQUEST' ) @dynamo_resource.client.wait_until(:table_exists, table_name: table_name) @table diff --git a/rustv1/cross_service/photo_asset_management/integration/tests/update_label.rs b/rustv1/cross_service/photo_asset_management/integration/tests/update_label.rs index 1fec6f2d329..664d541d8e4 100644 --- a/rustv1/cross_service/photo_asset_management/integration/tests/update_label.rs +++ b/rustv1/cross_service/photo_asset_management/integration/tests/update_label.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; -use aws_sdk_dynamodb::types::{AttributeDefinition, KeySchemaElement, ProvisionedThroughput}; +use aws_sdk_dynamodb::types::{AttributeDefinition, KeySchemaElement, BillingMode}; use aws_sdk_rekognition::types::Label; use photo_asset_management::{ common::{init_tracing_subscriber, Common}, @@ -27,13 +27,8 @@ async fn create_table(common: &Common) -> Result<(), impl std::error::Error> { .attribute_definitions( AttributeDefinition::builder() .attribute_name("Label") - .attribute_type(aws_sdk_dynamodb::types::ScalarAttributeType::S) - .build(), - ) - .provisioned_throughput( - ProvisionedThroughput::builder() - .write_capacity_units(1) - .read_capacity_units(1) + .attribute_type(aws_sdk_dynamodb::types::ScalarAttributeType::S), + .billing_mode(aws_sdk_dynamodb::types::BillingMode::PayPerRequest) .build(), ) .send() diff --git a/rustv1/examples/dynamodb/src/bin/crud.rs b/rustv1/examples/dynamodb/src/bin/crud.rs index 9a5e4b1ae83..1f515945ec7 100644 --- a/rustv1/examples/dynamodb/src/bin/crud.rs +++ b/rustv1/examples/dynamodb/src/bin/crud.rs @@ -8,7 +8,7 @@ use aws_sdk_dynamodb::error::SdkError; use aws_sdk_dynamodb::operation::create_table::CreateTableError; use aws_sdk_dynamodb::operation::put_item::PutItemError; use aws_sdk_dynamodb::types::{ - AttributeDefinition, AttributeValue, KeySchemaElement, KeyType, ProvisionedThroughput, + AttributeDefinition, AttributeValue, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType, Select, TableStatus, }; use aws_sdk_dynamodb::{config::Region, meta::PKG_VERSION, Client, Error}; @@ -63,18 +63,12 @@ async fn make_table( .build() .expect("creating KeySchemaElement"); - let pt = ProvisionedThroughput::builder() - .read_capacity_units(10) - .write_capacity_units(5) - .build() - .expect("creating ProvisionedThroughput"); - match client .create_table() .table_name(table) .key_schema(ks) .attribute_definitions(ad) - .provisioned_throughput(pt) + .billing_mode(BillingMode::PayPerRequest) .send() .await { diff --git a/rustv1/examples/dynamodb/src/bin/dynamodb-helloworld.rs b/rustv1/examples/dynamodb/src/bin/dynamodb-helloworld.rs index 07b62c2d12e..999bb036507 100644 --- a/rustv1/examples/dynamodb/src/bin/dynamodb-helloworld.rs +++ b/rustv1/examples/dynamodb/src/bin/dynamodb-helloworld.rs @@ -5,7 +5,7 @@ use aws_config::meta::region::RegionProviderChain; use aws_sdk_dynamodb::types::{ - AttributeDefinition, KeySchemaElement, KeyType, ProvisionedThroughput, ScalarAttributeType, + AttributeDefinition, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType, }; use aws_sdk_dynamodb::{config::Region, meta::PKG_VERSION, Client, Error}; use clap::Parser; @@ -47,18 +47,12 @@ async fn create_table(client: &Client) -> Result<(), Error> { .build() .expect("creating AttributeDefinition"); - let pt = ProvisionedThroughput::builder() - .write_capacity_units(10) - .read_capacity_units(10) - .build() - .expect("creating ProvisionedThroughput"); - let new_table = client .create_table() .table_name("test-table") .key_schema(ks) .attribute_definitions(ad) - .provisioned_throughput(pt) + .billing_mode(BillingMode::PayPerRequest) .send() .await?; println!( diff --git a/rustv1/examples/dynamodb/src/bin/partiql.rs b/rustv1/examples/dynamodb/src/bin/partiql.rs index 1a1305d085b..e4e67486f97 100644 --- a/rustv1/examples/dynamodb/src/bin/partiql.rs +++ b/rustv1/examples/dynamodb/src/bin/partiql.rs @@ -8,7 +8,7 @@ use aws_sdk_dynamodb::error::SdkError; use aws_sdk_dynamodb::operation::create_table::CreateTableError; use aws_sdk_dynamodb::operation::execute_statement::ExecuteStatementError; use aws_sdk_dynamodb::types::{ - AttributeDefinition, AttributeValue, KeySchemaElement, KeyType, ProvisionedThroughput, + AttributeDefinition, AttributeValue, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType, TableStatus, }; use aws_sdk_dynamodb::{config::Region, meta::PKG_VERSION, Client, Error}; @@ -44,7 +44,7 @@ fn random_string(n: usize) -> String { .collect() } -/// Create a new table. +/// Create a new on-demand table. // snippet-start:[dynamodb.rust.partiql-make_table] async fn make_table( client: &Client, @@ -63,18 +63,12 @@ async fn make_table( .build() .expect("creating KeySchemaElement"); - let pt = ProvisionedThroughput::builder() - .read_capacity_units(10) - .write_capacity_units(5) - .build() - .expect("creating ProvisionedThroughput"); - match client .create_table() .table_name(table) .key_schema(ks) .attribute_definitions(ad) - .provisioned_throughput(pt) + .billing_mode(BillingMode::PayPerRequest) .send() .await { diff --git a/rustv1/examples/dynamodb/src/scenario/create.rs b/rustv1/examples/dynamodb/src/scenario/create.rs index ea87771e641..ec127a1538c 100644 --- a/rustv1/examples/dynamodb/src/scenario/create.rs +++ b/rustv1/examples/dynamodb/src/scenario/create.rs @@ -4,11 +4,11 @@ use crate::scenario::error::Error; use aws_sdk_dynamodb::operation::create_table::CreateTableOutput; use aws_sdk_dynamodb::types::{ - AttributeDefinition, KeySchemaElement, KeyType, ProvisionedThroughput, ScalarAttributeType, + AttributeDefinition, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType, }; use aws_sdk_dynamodb::Client; -// Create a table. +// Create an on-demand table. // snippet-start:[dynamodb.rust.create-table] pub async fn create_table( client: &Client, @@ -30,18 +30,12 @@ pub async fn create_table( .build() .map_err(Error::BuildError)?; - let pt = ProvisionedThroughput::builder() - .read_capacity_units(10) - .write_capacity_units(5) - .build() - .map_err(Error::BuildError)?; - let create_table_response = client .create_table() .table_name(table_name) .key_schema(ks) .attribute_definitions(ad) - .provisioned_throughput(pt) + .billing_mode(BillingMode::PayPerRequest) .send() .await; diff --git a/rustv1/examples/dynamodb/src/scenario/movies/startup.rs b/rustv1/examples/dynamodb/src/scenario/movies/startup.rs index 646c5bf4a24..246595c8026 100644 --- a/rustv1/examples/dynamodb/src/scenario/movies/startup.rs +++ b/rustv1/examples/dynamodb/src/scenario/movies/startup.rs @@ -5,8 +5,8 @@ use crate::scenario::error::Error; use aws_sdk_dynamodb::{ operation::create_table::builders::CreateTableFluentBuilder, types::{ - AttributeDefinition, KeySchemaElement, KeyType, ProvisionedThroughput, ScalarAttributeType, - TableStatus, WriteRequest, + AttributeDefinition, KeySchemaElement, KeyType, ScalarAttributeType, TableStatus, + WriteRequest, }, Client, }; @@ -14,8 +14,6 @@ use futures::future::join_all; use std::{collections::HashMap, time::Duration}; use tracing::{debug, info, trace}; -const CAPACITY: i64 = 10; - #[tracing::instrument(level = "trace")] pub async fn initialize(client: &Client, table_name: &str) -> Result<(), Error> { info!("Initializing Movies DynamoDB in {table_name}"); @@ -24,7 +22,7 @@ pub async fn initialize(client: &Client, table_name: &str) -> Result<(), Error> info!("Found existing table {table_name}"); } else { info!("Table does not exist, creating {table_name}"); - create_table(client, table_name, "year", "title", CAPACITY)? + create_table(client, table_name, "year", "title")? .send() .await?; await_table(client, table_name).await?; @@ -55,9 +53,8 @@ pub fn create_table( table_name: &str, primary_key: &str, sort_key: &str, - capacity: i64, ) -> Result { - info!("Creating table: {table_name} with capacity {capacity} and key structure {primary_key}:{sort_key}"); + info!("Creating table: {table_name} key structure {primary_key}:{sort_key}"); Ok(client .create_table() .table_name(table_name) @@ -89,13 +86,7 @@ pub fn create_table( .build() .expect("Failed to build attribute definition"), ) - .provisioned_throughput( - ProvisionedThroughput::builder() - .read_capacity_units(capacity) - .write_capacity_units(capacity) - .build() - .expect("Failed to specify ProvisionedThroughput"), - )) + .billing_mode(aws_sdk_dynamodb::types::BillingMode::PayPerRequest)) } // snippet-end:[dynamodb.rust.movies-create_table_request] From 8a3807eb6908cbfa036ff32c2e070d64e6a3030c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:03:32 +0000 Subject: [PATCH 091/144] Bump uri from 0.12.2 to 0.12.4 in /ruby (#7271) Bumps [uri](https://github.com/ruby/uri) from 0.12.2 to 0.12.4. - [Release notes](https://github.com/ruby/uri/releases) - [Commits](https://github.com/ruby/uri/compare/v0.12.2...v0.12.4) --- updated-dependencies: - dependency-name: uri dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ruby/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index f758ba18ef2..813cdea8b4a 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -1515,7 +1515,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - uri (0.12.2) + uri (0.12.4) zip (2.0.2) PLATFORMS From 8d354d466c278bda8fddba3c2843ea444148ade7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:06:14 +0000 Subject: [PATCH 092/144] Bump rack from 3.1.10 to 3.1.11 in /ruby (#7273) Bumps [rack](https://github.com/rack/rack) from 3.1.10 to 3.1.11. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.1.10...v3.1.11) --- updated-dependencies: - dependency-name: rack dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ruby/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 813cdea8b4a..c6e4d1bcb82 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -1441,7 +1441,7 @@ GEM prettyprint prettyprint (0.1.1) racc (1.8.1) - rack (3.1.10) + rack (3.1.11) rack-protection (4.1.1) base64 (>= 0.1.0) logger (>= 1.6.0) From a49e6e28cffacf00ee9d4456ba469b5d8cee5ad3 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 4 Mar 2025 15:42:32 -0500 Subject: [PATCH 093/144] rolled in review comments --- javav2/example_code/entityresolution/pom.xml | 11 ++ .../entity/scenario/EntityResActions.java | 167 +++++++++++++++++- .../entity/scenario/EntityResScenario.java | 15 +- 3 files changed, 179 insertions(+), 14 deletions(-) diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index 19684620c48..a70292a446b 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -80,10 +80,21 @@ software.amazon.awssdk entityresolution + + com.opencsv + opencsv + 5.7.1 + software.amazon.awssdk s3 + + + org.fusesource.jansi + jansi + 2.4.0 + software.amazon.awssdk netty-nio-client diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 09f55e8e8c4..00c06d5554b 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -3,7 +3,11 @@ package com.example.entity.scenario; +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvException; +import org.fusesource.jansi.AnsiConsole; import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; @@ -40,18 +44,30 @@ import software.amazon.awssdk.services.entityresolution.model.ValidationException; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; + +import java.io.IOException; +import java.io.StringReader; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.fusesource.jansi.Ansi.ansi; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { + + private static final String PREFIX = "eroutput/"; private static final Logger logger = LoggerFactory.getLogger(EntityResActions.class); private static EntityResolutionAsyncClient entityResolutionAsyncClient; @@ -594,5 +610,152 @@ public void uploadInputData(String bucketName, String jsonData, String csvData) }).join(); } -// snippet-end:[entityres.java2_actions.main] -} \ No newline at end of file + + /** + * Finds the latest file in the S3 bucket that starts with "run-" in any depth of subfolders + */ + private CompletableFuture findLatestMatchingFile(String bucketName) { + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucketName) + .prefix(PREFIX) // Searches within the given folder + .build(); + + return getS3AsyncClient().listObjectsV2(request) + .thenApply(response -> response.contents().stream() + .map(S3Object::key) + .filter(key -> key.matches(".*?/run-[0-9a-zA-Z\\-]+")) // Matches files like run-XXXXX in any subfolder + .max(String::compareTo) // Gets the latest file + .orElse(null)) + .whenComplete((result, exception) -> { + if (exception == null) { + if (result != null) { + logger.info("Latest matching file found: " + result); + } else { + logger.info("No matching files found."); + } + } else { + throw new CompletionException("Failed to find latest matching file: " + exception.getMessage(), exception); + } + }); + } + + /** + * Prints the data located in the file in the S3 bucket that starts with "run-" in any depth of subfolders + */ + public void printData(String bucketName) { + try { + // Find the latest file with "run-" prefix in any depth of subfolders. + String s3Key = findLatestMatchingFile(bucketName).join(); + if (s3Key == null) { + logger.error("No matching files found in S3."); + return; + } + + logger.info("Downloading file: " + s3Key); + + // Read CSV file as String. + String csvContent = readCSVFromS3Async(bucketName, s3Key).join(); + if (csvContent.isEmpty()) { + logger.error("File is empty."); + return; + } + + // Process CSV content. + List records = parseCSV(csvContent); + printTable(records); + + } catch (RuntimeException | IOException | CsvException e) { + logger.error("Error processing CSV file from S3: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Reads a CSV file from S3 and returns it as a String. + */ + private static CompletableFuture readCSVFromS3Async(String bucketName, String s3Key) { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(); + + // Initiating the asynchronous request to get the file as bytes + return getS3AsyncClient().getObject(getObjectRequest, AsyncResponseTransformer.toBytes()) + .thenApply(responseBytes -> responseBytes.asUtf8String()) // Convert bytes to UTF-8 string + .whenComplete((result, exception) -> { + if (exception != null) { + throw new CompletionException("Failed to read CSV from S3: " + exception.getMessage(), exception); + } else { + logger.info("Successfully fetched CSV file content from S3."); + } + }); + } + + /** + * Parses CSV content from a String into a list of records. + */ + private static List parseCSV(String csvContent) throws IOException, CsvException { + try (CSVReader csvReader = new CSVReader(new StringReader(csvContent))) { + return csvReader.readAll(); + } + } + + /** + * Prints the given CSV data in a formatted table + */ + private static void printTable(List records) { + if (records.isEmpty()) { + logger.info("No records found."); + return; + } + + String[] headers = records.get(0); + List rows = records.subList(1, records.size()); + + // Determine column widths dynamically based on longest content + int[] columnWidths = new int[headers.length]; + for (int i = 0; i < headers.length; i++) { + final int columnIndex = i; + int maxWidth = Math.max(headers[i].length(), rows.stream() + .map(row -> row.length > columnIndex ? row[columnIndex].length() : 0) + .max(Integer::compareTo) + .orElse(0)); + columnWidths[i] = Math.min(maxWidth, 25); // Limit max width for better readability + } + + // Enable ANSI Console for colored output + AnsiConsole.systemInstall(); + + // Print table header + logger.info(String.valueOf(ansi().fgYellow().a("=== CSV Data from S3 ===").reset())); + printRow(headers, columnWidths, true); + + // Print rows + rows.forEach(row -> printRow(row, columnWidths, false)); + + // Restore console to normal + AnsiConsole.systemUninstall(); + } + + private static void printRow(String[] row, int[] columnWidths, boolean isHeader) { + String border = IntStream.range(0, columnWidths.length) + .mapToObj(i -> "-".repeat(columnWidths[i] + 2)) + .collect(Collectors.joining("+", "+", "+")); + + if (isHeader) { + logger.info(border); + } + + logger.info("|"); + for (int i = 0; i < columnWidths.length; i++) { + String cell = (i < row.length && row[i] != null) ? row[i] : ""; + logger.info(" %-" + columnWidths[i] + "s |", isHeader ? ansi().fgBrightBlue().a(cell).reset() : cell); + } + System.out.println(); + + if (isHeader) { + logger.info(border); + } + } +} +// snippet-end:[entityres.java2_actions.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index cb6067b1662..75f7dfc26f4 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -342,19 +342,10 @@ private static void runScenario() throws InterruptedException { logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); logger.info("The following represents the output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); + actions.printData(glueBucketName); + logger.info(""" - - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable Mary Major mary_major@company.com, 555-222-3333 4 ec05e7a55a0d4319b86da0a65286118f000040 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 María García maría_garcia@company.com 555-567-1234 3 201ed8241ec04f9aa7fcfd962220580500001369367187456 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 1 Jane Doe jane.doe@example.com 1 895c3a439dc44a298663d52c08635e1a0000434359738368 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 1 Jane B.Doe jane.doe@example.com 1 69c2b2190c60427c8f5a2daa7ce5d45b00001463856467968 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.8914204 2 John Doe john.doe@example.com 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s - arn:aws:glue:us-east-1::xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.8914204 2 John Doe Jr. john.doe@example.com 555-654-3210 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s - + Note that each of the last 2 records are considered a match even though the 'name' differs between the records; For example 'John Doe Jr.' compared to 'John Doe'. The confidence level is a value between 0 and 1, where 1 indicates a perfect match. From 97ee2d091582aaa575084b9e8fcb8f416a80c66a Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 4 Mar 2025 16:20:37 -0500 Subject: [PATCH 094/144] updated the example --- .../example/entity/scenario/EntityResActions.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 00c06d5554b..b29a3cbec84 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -700,12 +700,15 @@ private static List parseCSV(String csvContent) throws IOException, Cs } } + /** + * Prints the given CSV data in a formatted table + */ /** * Prints the given CSV data in a formatted table */ private static void printTable(List records) { if (records.isEmpty()) { - logger.info("No records found."); + System.out.println("No records found."); return; } @@ -727,7 +730,7 @@ private static void printTable(List records) { AnsiConsole.systemInstall(); // Print table header - logger.info(String.valueOf(ansi().fgYellow().a("=== CSV Data from S3 ===").reset())); + System.out.println(ansi().fgYellow().a("=== CSV Data from S3 ===").reset()); printRow(headers, columnWidths, true); // Print rows @@ -743,18 +746,18 @@ private static void printRow(String[] row, int[] columnWidths, boolean isHeader) .collect(Collectors.joining("+", "+", "+")); if (isHeader) { - logger.info(border); + System.out.println(border); } - logger.info("|"); + System.out.print("|"); for (int i = 0; i < columnWidths.length; i++) { String cell = (i < row.length && row[i] != null) ? row[i] : ""; - logger.info(" %-" + columnWidths[i] + "s |", isHeader ? ansi().fgBrightBlue().a(cell).reset() : cell); + System.out.printf(" %-" + columnWidths[i] + "s |", isHeader ? ansi().fgBrightBlue().a(cell).reset() : cell); } System.out.println(); if (isHeader) { - logger.info(border); + System.out.println(border); } } } From 2aacd34caf2e17e7566eb69787a4506daca05445 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 09:20:48 -0500 Subject: [PATCH 095/144] add Entity Resolution files --- .../example_code/entityresolution/.gitignore | 38 ++ javav2/example_code/entityresolution/pom.xml | 91 +++++ .../entity/scenario/EntityResActions.java | 376 ++++++++++++++++++ .../entity/scenario/EntityResScenario.java | 261 ++++++++++++ .../src/main/resources/glue.yaml | 240 +++++++++++ 5 files changed, 1006 insertions(+) create mode 100644 javav2/example_code/entityresolution/.gitignore create mode 100644 javav2/example_code/entityresolution/pom.xml create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java create mode 100644 javav2/example_code/entityresolution/src/main/resources/glue.yaml diff --git a/javav2/example_code/entityresolution/.gitignore b/javav2/example_code/entityresolution/.gitignore new file mode 100644 index 00000000000..5ff6309b719 --- /dev/null +++ b/javav2/example_code/entityresolution/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml new file mode 100644 index 00000000000..f710bd02f78 --- /dev/null +++ b/javav2/example_code/entityresolution/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + org.example + entityresolution + 1.0-SNAPSHOT + + + UTF-8 + 17 + 17 + 17 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + IntegrationTest + + + + + + + + software.amazon.awssdk + bom + 2.29.45 + pom + import + + + + + + org.junit.jupiter + junit-jupiter-api + 5.9.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + software.amazon.awssdk + secretsmanager + + + com.google.code.gson + gson + 2.10.1 + + + org.junit.platform + junit-platform-commons + 1.9.2 + + + org.junit.platform + junit-platform-launcher + 1.9.2 + test + + + software.amazon.awssdk + entityresolution + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + cloudformation + + + + \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java new file mode 100644 index 00000000000..b2dfda3e4f6 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -0,0 +1,376 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity.scenario; + +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.entityresolution.EntityResolutionClient; +import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; + +import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowResponse; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; +import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; +import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; +import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; +import software.amazon.awssdk.services.entityresolution.model.InputSource; +import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; +import software.amazon.awssdk.services.entityresolution.model.OutputSource; +import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; +import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; +import software.amazon.awssdk.services.entityresolution.model.ResolutionType; +import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; +import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobResponse; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; + +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class EntityResActions { + + private EntityResolutionClient resolutionClient; + + private static EntityResolutionAsyncClient entityResolutionAsyncClient; + + private static S3AsyncClient s3AsyncClient; + + public static EntityResolutionAsyncClient getResolutionAsyncClient() { + if (entityResolutionAsyncClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return entityResolutionAsyncClient; + } + + + public static S3AsyncClient getS3AsyncClient() { + if (s3AsyncClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + s3AsyncClient = S3AsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return s3AsyncClient; + } + + // snippet-start:[entityres.java2_delete_matching_workflow.main] + /** + * Asynchronously deletes a workflow with the specified name. + * + * @param workflowName the name of the workflow to be deleted + * @return a {@link CompletableFuture} that completes when the workflow has been deleted + * @throws RuntimeException if the deletion of the workflow fails + */ + public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { + DeleteMatchingWorkflowRequest request = DeleteMatchingWorkflowRequest.builder() + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().deleteMatchingWorkflow(request) + .thenAccept(response -> { + // No response object, just log success + }) + .exceptionally(exception -> { + throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); + }); + } + // snippet-end:[entityres.java2_delete_matching_workflow.main] + + // snippet-start:[entityres.java2_create_schema.main] + /** + * Creates a schema mapping asynchronously. + * + * @param schemaName the name of the schema to create the mapping for + * @return a {@link CompletableFuture} that represents the asynchronous creation of the schema mapping + */ + public CompletableFuture createSchemaMappingAsync(String schemaName) { + List schemaAttributes = List.of( + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type("UNIQUE_ID").build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type("STRING").build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type("STRING").build() + ); + + CreateSchemaMappingRequest request = CreateSchemaMappingRequest.builder() + .schemaName(schemaName) + .mappedInputFields(schemaAttributes) + .build(); + + return getResolutionAsyncClient().createSchemaMapping(request) + .whenComplete((response, exception) -> { + if (response != null) { + System.out.println("Schema Mapping Created Successfully!"); + } else { + throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); + } + }); + } + // snippet-end:[entityres.java2_create_schema.main] + + // snippet-start:[entityres.java2_get_schema_mapping.main] + /** + * Retrieves the schema mapping asynchronously. + * + * @param schemaName the name of the schema to retrieve the mapping for + * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation is complete + * @throws RuntimeException if the schema mapping retrieval fails + */ + public CompletableFuture getSchemaMappingAsync(String schemaName) { + GetSchemaMappingRequest mappingRequest = GetSchemaMappingRequest.builder() + .schemaName(schemaName) + .build(); + + return getResolutionAsyncClient().getSchemaMapping(mappingRequest) + .whenComplete((response, exception) -> { + if (response != null) { + response.mappedInputFields().forEach(attribute -> + System.out.println("Attribute Name: " + attribute.fieldName() + + ", Attribute Type: " + attribute.type().toString())); + } else { + throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); + } + }); + } + // snippet-end:[entityres.java2_get_schema_mapping.main] + + // snippet-start:[entityres.java2_get_job.main] + /** + * Asynchronously retrieves a matching job based on the provided job ID and workflow name. + * + * @param jobId the ID of the job to retrieve + * @param workflowName the name of the workflow associated with the job + * @return a {@link CompletableFuture} that completes when the job information is available or an exception occurs + */ + public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { + GetMatchingJobRequest request = GetMatchingJobRequest.builder() + .jobId(jobId) + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().getMatchingJob(request) + .thenAccept(response -> { + System.out.println("Job status: " + response.status()); + System.out.println("Job details: " + response.toString()); + }) + .exceptionally(ex -> { + throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); + }); + } + // snippet-end:[entityres.java2_get_job.main] + + // snippet-start:[entityres.java2_start_job.main] + /** + * Starts a matching job asynchronously for the specified workflow name. + * + * @param workflowName the name of the workflow for which to start the matching job + * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty string if the operation fails + */ + public CompletableFuture startMatchingJobAsync(String workflowName) { + StartMatchingJobRequest jobRequest = StartMatchingJobRequest.builder() + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().startMatchingJob(jobRequest) + .whenComplete((response, exception) -> { + if (response != null) { + // Get the job ID from the response + String jobId = response.jobId(); + System.out.println("Job ID: " + jobId); + } else { + // Handle the exception if the response is null + throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response != null ? response.jobId() : ""); + } + // snippet-end:[entityres.java2_start_job.main] + + // snippet-start:[entityres.java2_check_matching_workflow.main] + /** + * Checks the status of a workflow asynchronously. + * + * @param jobId the ID of the job to check + * @param workflowName the name of the workflow to check + * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed successfully + */ + public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { + GetMatchingJobRequest request = GetMatchingJobRequest.builder() + .jobId(jobId) + .workflowName(workflowName) + .build(); + + return getResolutionAsyncClient().getMatchingJob(request) + .thenApply(response -> { + System.out.println("\nJob status: " + response.status()); + return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); + }) + .exceptionally(exception -> { + System.out.println("Error checking workflow status: " + exception.getMessage()); + return false; + }); + } + // snippet-end:[entityres.java2_check_matching_workflow.main] + + // snippet-start:[entityres.java2_create_matching_workflow.main] + /** + * Creates an asynchronous CompletableFuture to manage the creation of a matching workflow. + * + * @param roleARN the AWS IAM role ARN to be used for the workflow execution + * @param workflowName the name of the workflow to be created + * @param outputBucket the S3 bucket path where the workflow output will be stored + * @param inputGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source + * @param schemaName the name of the schema to be used for the input source + * @return a CompletableFuture that, when completed, will return the ARN of the created workflow + */ + public CompletableFuture createMatchingWorkflowAsync(String roleARN, String workflowName, String outputBucket, String inputGlueTableArn, String schemaName) { + InputSource inputSource = InputSource.builder() + .inputSourceARN(inputGlueTableArn) + .schemaName(schemaName) + .build(); + + OutputAttribute outputAttribute = OutputAttribute.builder() + .name("id") + .build(); + + OutputSource outputSource = OutputSource.builder() + .outputS3Path(outputBucket) + .output(outputAttribute) + .build(); + + ResolutionTechniques type = ResolutionTechniques.builder() + .resolutionType(ResolutionType.ML_MATCHING) + .build(); + + CreateMatchingWorkflowRequest workflowRequest = CreateMatchingWorkflowRequest.builder() + .roleArn(roleARN) + .description("Created by using the AWS SDK for Java") + .workflowName(workflowName) + .inputSourceConfig(List.of(inputSource)) + .outputSourceConfig(List.of(outputSource)) + .resolutionTechniques(type) + .build(); + + return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) + .whenComplete((response, exception) -> { + if (response != null) { + System.out.println("Workflow created successfully with ID: " + response.workflowArn()); + } else { + throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); + } + }) + .thenApply(CreateMatchingWorkflowResponse::workflowArn); + } + // snippet-end:[entityres.java2_create_matching_workflow.main] + + + /** + * Uploads a local file to an Amazon S3 bucket asynchronously. + * + * @param bucketName the name of the S3 bucket to upload the file to + * @param json the JSON data to be uploaded + * @return a {@link CompletableFuture} representing the asynchronous operation of uploading the file + * @throws RuntimeException if an error occurs during the file upload + */ + public CompletableFuture uploadLocalFileAsync(String bucketName, String json) { + + String key = "data/data.json"; // Corrected: No leading "/" + PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType("application/json") + .build(); + + CompletableFuture response = getS3AsyncClient().putObject(objectRequest, AsyncRequestBody.fromString(json)); + return response.whenComplete((resp, ex) -> { + if (ex != null) { + throw new RuntimeException("Failed to upload file", ex); + } + }); + } + + /** + * Checks if a specific object exists in an Amazon S3 bucket. + * + * @param bucketName the name of the S3 bucket to check + * @return true if the object exists, false otherwise + */ + public boolean doesObjectExist(String bucketName) { + try { + String key = "data/data.json"; + getS3AsyncClient().headObject(HeadObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build()); + return true; // File exists + + } catch (S3Exception e) { + return false; + } + } + +} diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java new file mode 100644 index 00000000000..9a6705150ae --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -0,0 +1,261 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity.scenario; + + +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.CompletionException; + +public class EntityResScenario { + public static final String DASHES = new String(new char[80]).replace("\0", "-"); + private static final String ROLES_STACK = "EntityResolutionCdkStack"; + public static void main (String[]args) throws InterruptedException { + + final String usage = """ + + Usage: + + + Where: + workflowName - A unique identifier for the matching workflow, used in the entity resolution process. + schemaName - The name of the schema, which defines the structure and attributes for the data being processed. + roleARN: The ARN of the IAM role, that grants permissions for the entity resolution workflow (this resource is created using the CDK script. See the Readme). + dataS3bucket: The S3 bucket,that stores the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. + outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. + inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. + """; + String workflowName = "MyMatchingWorkflow433"; + String schemaName = "schema232"; + + // Use the AWS CDK to create this AWS resources. See the Readme file. + String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; + String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; + String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + + EntityResActions actions = new EntityResActions(); + Scanner scanner = new Scanner(System.in); + System.out.println("Welcome to the AWS Entity Resolution Scenario. "); + System.out.println(""" + AWS Entity Resolution is a fully-managed machine learning service provided by + Amazon Web Services (AWS) that helps organizations extract, link, and + organize information from multiple data sources. It leverages natural + language processing and deep learning models to identify and resolve + entities, such as people, places, organizations, and products, + across structured and unstructured data. + + With Entity Resolution, customers can build robust data integration + pipelines to combine and reconcile data from multiple systems, databases, + and documents. The service can handle ambiguous, incomplete, or conflicting + information, and provide a unified view of entities and their relationships. + This can be particularly valuable in applications such as customer 360, + fraud detection, supply chain management, and knowledge management, where + accurate entity identification is crucial. + + The `EntityResolutionAsyncClient` interface in the AWS SDK for Java 2.x + provides a set of methods to programmatically interact with the AWS Entity + Resolution service. This allows developers to automate the entity extraction, + linking, and deduplication process as part of their data processing workflows. + With Entity Resolution, organizations can unlock the value of their data, + improve decision-making, and enhance customer experiences by having a reliable, + comprehensive view of their key entities. + """); + + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + + /* + This JSON is a valid input for the AWS Entity Resolution service. + The JSON represents an array of three objects, each containing an "id", "name", and "email" + property. This format aligns with the expected input structure for the + Entity Resolution service. + */ + String json = """ + [ + { + "id": "1", + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + { + "id": "2", + "name": "Bob Smith", + "email": "bob.smith@example.com" + }, + { + "id": "3", + "name": "Charlie Black", + "email": "charlie.black@example.com" + } + ] + """; + System.out.println("Upload the JSON to the "+ dataS3bucket +" S3 bucket if it does not exist"); + System.out.println(json); + waitForInputToContinue(scanner); + if (!actions.doesObjectExist(dataS3bucket)) { + actions.uploadLocalFileAsync(dataS3bucket, json); + } else { + System.out.println("The JSON exists in "+ dataS3bucket); + } + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("Create Schema Mapping"); + System.out.println(""" + Entity Resolution Schema Mapping aligns and integrates data from + multiple sources by identifying and matching corresponding entities + like customers or products. It unifies schemas, resolves conflicts, + and uses machine learning to link related entities, enabling a + consolidated, accurate view for improved data quality and decision-making. + + In this example, the schema mapping lines up with the fields in the JSON. That is, + it contains these fields: id, name, and email. + """); + waitForInputToContinue(scanner); + try { + actions.createSchemaMappingAsync(schemaName).join(); + System.out.println("Schema mapping was successfully created."); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("Create an AWS Entity Resolution Workflow. "); + System.out.println(""" + An Entity Resolution matching workflow identifies and links records + across datasets that represent the same real-world entity, such as + customers or products. Using techniques like schema mapping, + data profiling, and machine learning algorithms, + it evaluates attributes like names or emails to detect duplicates + or relationships, even with variations or inconsistencies. + The workflow outputs consolidated, de-duplicated data,\s + """); + waitForInputToContinue(scanner); + try { + String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + System.out.println("The workflow was successfully created. The ARN is: " + workflowArn); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + waitForInputToContinue(scanner); + + System.out.println(DASHES); + System.out.println("3. Start the matching job of the " +workflowName +" workflow."); + waitForInputToContinue(scanner); + String jobId = null; + try { + jobId = actions.startMatchingJobAsync(workflowName).join(); + System.out.println("The matching job was successfully started. Job ID: " + jobId); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("4. Get job details."); + waitForInputToContinue(scanner); + actions.getMatchingJobAsync(jobId, workflowName); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("5. Get Schema Mapping."); + waitForInputToContinue(scanner); + try { + actions.getSchemaMappingAsync(schemaName).join(); + System.out.println("Schema mapping retrieval completed."); + } catch (CompletionException ce) { + System.err.println("Error retrieving schema mapping: " + ce.getCause().getMessage()); + } + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("9. Delete the AWS Entity Resolution Workflow."); + System.out.println(""" + You cannot delete a workflow that is in a running state. + Would you like to wait for the workflow to complete. + This can take up to 30 mins (y/n). + """); + String delAns = scanner.nextLine().trim(); + if (delAns.equalsIgnoreCase("y")) { + System.out.println("You selected to delete Entity Resolution Workflow."); + waitForInputToContinue(scanner); + countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + try { + actions.deleteMatchingWorkflowAsync(workflowName).join(); + System.out.println("Workflow deleted successfully!"); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + // CloudFormationHelper.destroyCloudFormationStack(ROLES_STACK); + } + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("This concludes the AWS Entity Resolution scenario."); + System.out.println(DASHES); + + + } + + private static void waitForInputToContinue(Scanner scanner) { + while (true) { + System.out.println(""); + System.out.println("Enter 'c' followed by to continue:"); + String input = scanner.nextLine(); + + if (input.trim().equalsIgnoreCase("c")) { + System.out.println("Continuing with the program..."); + System.out.println(""); + break; + } else { + // Handle invalid input. + System.out.println("Invalid input. Please try again."); + } + } + } + + public static void countdownWithWorkflowCheck(EntityResActions actions, int totalSeconds, String jobId, String workflowName) throws InterruptedException { + int secondsElapsed = 0; + + while (true) { + // Calculate display minutes and seconds + int remainingTime = totalSeconds - secondsElapsed; + int displayMinutes = remainingTime / 60; + int displaySeconds = remainingTime % 60; + + // Print the countdown + System.out.printf("\r%02d:%02d", displayMinutes, displaySeconds); + Thread.sleep(1000); // Wait for 1 second + secondsElapsed++; + + // Check workflow status every 60 seconds + if (secondsElapsed % 60 == 0 || remainingTime <= 0) { + if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { + System.out.println(); // Move to the next line after countdown + System.out.println("Countdown complete: Workflow is in SUCCEEDED state!"); + break; + } + } + + // If countdown reaches zero, reset it for continuous countdown + if (remainingTime <= 0) { + secondsElapsed = 0; + } + } + } + +} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/glue.yaml b/javav2/example_code/entityresolution/src/main/resources/glue.yaml new file mode 100644 index 00000000000..d09227c86fe --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/glue.yaml @@ -0,0 +1,240 @@ +Resources: + GlueDataBucket278CFAC6: + Type: AWS::S3::Bucket + Properties: + BucketName: glue-2cf5649393c7465f926ae00d0592eba8 + VersioningConfiguration: + Status: Enabled + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + Metadata: + aws:cdk:path: EntityResolutionCdkStack/GlueDataBucket/Resource + GlueDatabase: + Type: AWS::Glue::Database + Properties: + CatalogId: + Ref: AWS::AccountId + DatabaseInput: + Name: entity_resolution_db + Metadata: + aws:cdk:path: EntityResolutionCdkStack/GlueDatabase + GlueTable: + Type: AWS::Glue::Table + Properties: + CatalogId: + Ref: AWS::AccountId + DatabaseName: + Ref: GlueDatabase + TableInput: + Name: entity_resolution + StorageDescriptor: + Columns: + - Name: id + Type: string + - Name: name + Type: string + - Name: email + Type: string + InputFormat: org.apache.hadoop.mapred.TextInputFormat + Location: + Fn::Join: + - "" + - - s3:// + - Ref: GlueDataBucket278CFAC6 + - /data/ + OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat + SerdeInfo: + Parameters: + serialization.format: "1" + SerializationLibrary: org.openx.data.jsonserde.JsonSerDe + TableType: EXTERNAL_TABLE + DependsOn: + - GlueDatabase + Metadata: + aws:cdk:path: EntityResolutionCdkStack/GlueTable + EntityResolutionRoleB51A51D3: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: entityresolution.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/AmazonS3FullAccess + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/AWSEntityResolutionConsoleFullAccess + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/AWSGlueConsoleFullAccess + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSGlueServiceRole + Metadata: + aws:cdk:path: EntityResolutionCdkStack/EntityResolutionRole/Resource + EntityResolutionRoleDefaultPolicy586C8066: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - entityresolution:GetMatchingWorkflow + - entityresolution:StartMatchingWorkflow + Effect: Allow + Resource: "*" + Version: "2012-10-17" + PolicyName: EntityResolutionRoleDefaultPolicy586C8066 + Roles: + - Ref: EntityResolutionRoleB51A51D3 + Metadata: + aws:cdk:path: EntityResolutionCdkStack/EntityResolutionRole/DefaultPolicy/Resource + OutputBucket7114EB27: + Type: AWS::S3::Bucket + Properties: + BucketName: entity-resolution-output-entityresolutioncdkstack + VersioningConfiguration: + Status: Enabled + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + Metadata: + aws:cdk:path: EntityResolutionCdkStack/OutputBucket/Resource + CDKMetadata: + Type: AWS::CDK::Metadata + Properties: + Analytics: v2:deflate64:H4sIAAAAAAAA/02MMQ+CMBSEfwt7eYLEuFsnFwy6m0cp5klpDW0lpul/N7SL0919d7k91M0BqgJXW4phKhX1EG4OxcRwtY9gGwgnLybpGB91dpE9lZcQ+KjP6LBHK7fyjr2SkRHOEDqjEkt6NYrEd4vZxcg6aY1fRNq03r19uv+n3OiBHBkd2QU/uKuPUEHdFC9LVC5eO5oldFl/L3LHkcUAAAA= + Metadata: + aws:cdk:path: EntityResolutionCdkStack/CDKMetadata/Default + Condition: CDKMetadataAvailable +Outputs: + EntityResolutionArn: + Description: The ARN of the Glue Role + Value: + Fn::GetAtt: + - EntityResolutionRoleB51A51D3 + - Arn + GlueTableArn: + Description: The ARN of the Glue Table + Value: + Fn::Join: + - "" + - - "arn:aws:glue:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :table/ + - Ref: GlueDatabase + - /entity_resolution + GlueDataBucketName: + Description: The name of the Glue Data Bucket + Value: + Ref: GlueDataBucket278CFAC6 +Conditions: + CDKMetadataAvailable: + Fn::Or: + - Fn::Or: + - Fn::Equals: + - Ref: AWS::Region + - af-south-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-east-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-northeast-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-northeast-2 + - Fn::Equals: + - Ref: AWS::Region + - ap-south-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-southeast-1 + - Fn::Equals: + - Ref: AWS::Region + - ap-southeast-2 + - Fn::Equals: + - Ref: AWS::Region + - ca-central-1 + - Fn::Equals: + - Ref: AWS::Region + - cn-north-1 + - Fn::Equals: + - Ref: AWS::Region + - cn-northwest-1 + - Fn::Or: + - Fn::Equals: + - Ref: AWS::Region + - eu-central-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-north-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-south-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-west-1 + - Fn::Equals: + - Ref: AWS::Region + - eu-west-2 + - Fn::Equals: + - Ref: AWS::Region + - eu-west-3 + - Fn::Equals: + - Ref: AWS::Region + - il-central-1 + - Fn::Equals: + - Ref: AWS::Region + - me-central-1 + - Fn::Equals: + - Ref: AWS::Region + - me-south-1 + - Fn::Equals: + - Ref: AWS::Region + - sa-east-1 + - Fn::Or: + - Fn::Equals: + - Ref: AWS::Region + - us-east-1 + - Fn::Equals: + - Ref: AWS::Region + - us-east-2 + - Fn::Equals: + - Ref: AWS::Region + - us-west-1 + - Fn::Equals: + - Ref: AWS::Region + - us-west-2 +Parameters: + BootstrapVersion: + Type: AWS::SSM::Parameter::Value + Default: /cdk-bootstrap/hnb659fds/version + Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] +Rules: + CheckBootstrapVersion: + Assertions: + - Assert: + Fn::Not: + - Fn::Contains: + - - "1" + - "2" + - "3" + - "4" + - "5" + - Ref: BootstrapVersion + AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI. + From 036bd201f8ef34a4f5d7bbfdbf16387031c11418 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 09:52:50 -0500 Subject: [PATCH 096/144] add a new location for Basic Specs --- scenarios/basics/entity_resolution/README.md | 39 +++ .../basics/entity_resolution/SPECIFICATION.md | 328 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 scenarios/basics/entity_resolution/README.md create mode 100644 scenarios/basics/entity_resolution/SPECIFICATION.md diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md new file mode 100644 index 00000000000..9df7e209432 --- /dev/null +++ b/scenarios/basics/entity_resolution/README.md @@ -0,0 +1,39 @@ +## Overview +This AWS IoT SiteWise Service basic scenario demonstrates how to interact with the AWS IoT SiteWise service using an AWS SDK. The scenario covers various operations such as creating an Asset Model, creating assets, sending data to assets, and retrieving data. + +## Key Operations + +1. **Create an AWS SiteWise Asset Model**: + - This step creates an AWS SiteWise Asset Model by invoking the `createAssetModel` method. + +2. **Create an AWS IoT SiteWise Asset**: + - This operation creates an AWS SiteWise asset. + +3. **Retrieve the property ID values**: + - To send data to an asset, we need to get the property ID values for the model properties. This scenario uses temperature and humidity properties. + +4. **Send data to an AWS IoT SiteWise Asset**: + - This operation sends data to an IoT SiteWise Asset. + +5. **Retrieve the value of the IoT SiteWise Asset property**: + - This operation gets data from the asset. + +**Note** See the Eng spec for a full listing of operations. + +## Resources + +This Basics scenario requires an IAM role that has permissions to work with the AWS IoT SiteWise service. The scenario creates this resource using a CloudFormation template. + +## Implementations + +This scenario example will be implemented in the following languages: + +- Java +- Python +- JavaScript + +## Additional Reading + +- [AWS IoT SiteWise Documentation](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/what-is-sitewise.html) + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md new file mode 100644 index 00000000000..3b764c87468 --- /dev/null +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -0,0 +1,328 @@ +# AWS Entity Resolution Service Scenario Specification + +## Overview +This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution using an AWS SDK. It demonstrates various tasks such as creating a Schema Mapping, creating an matching workflow, starting the workflow, and so on. Finally this scenario demonstrates how to clean up resources. Its purpose is to demonstrate how to get up and running with AWS Entity Resolution and an AWS SDK. + +## Resources +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database and a table, and two S3 buckets. A CDK script is provided to create these resources. + +## Hello AWS Entity Resolution +This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listIdMappingJobsPaginator` to demonstrate how you can read through workflow job information. + +## Basics Scenario Program Flow +The AWS Entity Resolution Basics scenario executes the following operations. + +1. **Create a Schema Mapping**: + - Description: Creates a schema mapping invoking the `createSchemaMapping` method. + - Exception Handling: Check to see if a `ConflictException` is thrown. + If it is thrown, display the information and end the program. + +2. **Create a Matching Workflow**: + - Description: Creates a new matching workflow, defining how entities should be resolved and matched.. + - The method `createMatchingWorkflow` is called. + - Exception Handling: Check to see if a `ConflictException` is thrown if a conflict in the current state of the resource exists. If so, + display the message and end the program. + +3. **Start Matching Workflow**: + - Description: Initiates a matching workflow to process entity resolution based on predefined configurations. + - The method `listAssetModelProperties` is called to retrieve the property ID values. + - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. There are not + many other useful exceptions for this specific call. If so, display the message and end the program. + +4. **Send data to an AWS IoT SiteWise Asset**: + - Description: This operation sends data to an IoT SiteWise Asset. + - This step uses the method `batchPutAssetPropertyValue`. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +5. **Retrieve the value of the IoT SiteWise Asset property**: + - Description: This operation gets data from the asset. + - This step uses the method `getAssetPropertyValue`. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +6. **Create an IoT SiteWise Portal**: + - Description: This operation creates an IoT SiteWise portal. + - The method `createPortal` is called. + - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. + +7. **Describe the Portal**: + - Description: This operation describes the portal and returns a URL for the portal. + - The method `describePortal` is called and returns the URL. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +8. **Create an IoT SiteWise Gateway**: + - Description: This operation creates an IoT SiteWise Gateway. + - The method `createGateway` is called. + - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. + +9. **Describe the IoT SiteWise Gateway**: + - Description: This operation describes the Gateway. + - The method `describeGateway` is called. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. + +10. **Delete the AWS IoT SiteWise Assets**: + - The `delete` methods are called to clean up the resources. + - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program." + +### Program execution +The following shows the output of the AWS IoT SiteWise Basics scenario in the console. + +``` +AWS IoT SiteWise is a fully managed industrial software-as-a-service (SaaS) that +makes it easy to collect, store, organize, and monitor data from industrial equipment and processes. +It is designed to help industrial and manufacturing organizations collect data from their equipment and +processes, and use that data to make informed decisions about their operations. + +One of the key features of AWS IoT SiteWise is its ability to connect to a wide range of industrial +equipment and systems, including programmable logic controllers (PLCs), sensors, and other +industrial devices. It can collect data from these devices and organize it into a unified data model, +making it easier to analyze and gain insights from the data. AWS IoT SiteWise also provides tools for +visualizing the data, setting up alarms and alerts, and generating reports. + +Another key feature of AWS IoT SiteWise is its ability to scale to handle large volumes of data. +It can collect and store data from thousands of devices and process millions of data points per second, +making it suitable for large-scale industrial operations. Additionally, AWS IoT SiteWise is designed +to be secure and compliant, with features like role-based access controls, data encryption, +and integration with other AWS services for additional security and compliance features. + +Let's get started... + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +Use AWS CloudFormation to create an IAM role that are required for this scenario. +Stack creation requested, ARN is arn:aws:cloudformation:us-east-1:814548047983:stack/RoleSitewise/29f480c0-75fd-11ef-a42e-12cd4e534049 +Stack created successfully +-------------------------------------------------------------------------------- +1. Create an AWS SiteWise Asset Model + An AWS IoT SiteWise Asset Model is a way to represent the physical assets, such as equipment, + processes, and systems, that exist in an industrial environment. This model provides a structured and + hierarchical representation of these assets, allowing users to define the relationships and properties + of each asset. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The Asset Model MyAssetModel already exists. The id of the existing model is ffbc475b-73ad-4eb6-bf28-8728818fa8ef. Moving on... + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +2. Create an AWS IoT SiteWise Asset + The IoT SiteWise model defines the structure and metadata for your physical assets. Now we + can use the asset model to create the asset. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Asset created with ID: 4d681624-a303-46dd-8830-6189790ae915 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +3. Retrieve the property ID values + To send data to an asset, we need to get the property ID values for the + Temperature and Humidity properties. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The Humidity property Id is feb4aba6-55f9-4b00-b366-27b9d7e5a747 +The Temperature property Id is 6cb505aa-6bcc-46f4-a12a-7ca5df8eb028 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +4. Send data to an AWS IoT SiteWise Asset +By sending data to an IoT SiteWise Asset, you can aggregate data from +multiple sources, normalize the data into a standard format, and store it in a +centralized location. This makes it easier to analyze and gain insights from the data. + +This example demonstrate how to generate sample data and ingest it into the AWS IoT SiteWise asset. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Data sent successfully. + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +5. Retrieve the value of the IoT SiteWise Asset property +IoT SiteWise is an AWS service that allows you to collect, process, and analyze industrial data +from connected equipment and sensors. One of the key benefits of reading an IoT SiteWise property +is the ability to gain valuable insights from your industrial data. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The property name is: Temperature property +The value of this property is 23.5 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The property name is: Humidity property +The value of this property is 65.0 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +6. Create an IoT SiteWise Portal + An IoT SiteWise Portal allows you to aggregate data from multiple industrial sources, + such as sensors, equipment, and control systems, into a centralized platform. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Portal created successfully. Portal ID 63e65729-b7a1-410a-aa36-94145fe92153 +The portal Id is 63e65729-b7a1-410a-aa36-94145fe92153 + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +7. Describe the Portal + In this step, we will describe the step and provide the portal URL. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Portal URL: https://p-fy9qnrqy.app.iotsitewise.aws + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +8. Create an IoTSitewise Gateway +IoTSitewise Gateway serves as the bridge between industrial equipment, sensors, and the +cloud-based IoTSitewise service. It is responsible for securely collecting, processing, and +transmitting data from various industrial assets to the IoTSitewise platform, +enabling real-time monitoring, analysis, and optimization of industrial operations. + + + +Enter 'c' followed by to continue: +c +Continuing with the program... + +The ARN of the gateway is arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 +Gateway creation completed successfully. id is 50320670-1d88-4a7e-9013-1d7e8a3af832 +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +9. Describe the IoTSitewise Gateway + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Gateway Name: myGateway11 +Gateway ARN: arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 +Gateway Platform: GatewayPlatform(GreengrassV2=GreengrassV2(CoreDeviceThingName=myThing78)) +Gateway Creation Date: 2024-09-18T20:34:13.117Z +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +10. Delete the AWS IoT SiteWise Assets +Before you can delete the Asset Model, you must delete the assets. + + +Would you like to delete the IoT Sitewise Assets? (y/n) +y +You selected to delete the Sitewise assets. + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Portal 63e65729-b7a1-410a-aa36-94145fe92153 was deleted successfully. +An unexpected error occurred: Cannot invoke "java.util.concurrent.CompletableFuture.join()" because "future" is null +Asset deleted successfully. +Lets wait 1 min for the asset to be deleted +01:00The Gateway was deleted successfully +00:00Countdown complete! + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Delete the AWS IoT SiteWise Asset Model +Asset model deleted successfully. + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +Delete stack requested .... +Stack deleted successfully. +This concludes the AWS SiteWise Scenario +-------------------------------------------------------------------------------- + + +``` + +## SOS Tags + +The following table describes the metadata used in this Basics Scenario. + + +| action | metadata file | metadata key | +|--------------------------------|-----------------------------------|---------------------------------------- | +| `describeGateway` | iot_sitewise_metadata.yaml | iotsitewise_DescribeGateway | +| `deleteGateway ` | iot_sitewise_metadata.yaml | iotsitewise_DeleteGateway | +| `createGateway ` | iot_sitewise_metadata.yaml | iotsitewise_CreateGateway | +| `describePortal` | iot_sitewise_metadata.yaml | iotsitewise_DescribePortal | +| `listAssetModels` | iot_sitewise_metadata.yaml | iotsitewise_ListAssetModels | +| `deletePortal` | iot_sitewise_metadata.yaml | iotsitewise_DeletePortal | +| `createPortal` | iot_sitewise_metadata.yaml | iotsitewise_CreatePortal | +| `deleteAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAssetModel | +| `deleteAsset` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAsset | +| `describeAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DescribeAssetModel | +| `getAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_GetAssetPropertyValue | +| `batchPutAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_BatchPutAssetPropertyValue | +| `createAsset` | iot_sitewise_metadata.yaml | iotsitewise_CreateAsset | +| `createAssetModel ` | iot_sitewise_metadata.yaml | iotsitewise_CreateAssetModel | +| `scenario` | iot_sitewise_metadata.yaml | iotsitewise_Scenario | +| `hello` | iot_sitewise_metadata.yaml | iotsitewise_Hello | + + + From 6af3f28d3a1a746bf8f64b9a3d7fe80c941fcfe6 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 10:56:21 -0500 Subject: [PATCH 097/144] updated the Basic Specs --- .../basics/entity_resolution/SPECIFICATION.md | 92 ++++++++----------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 3b764c87468..881d0df2e32 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -25,46 +25,38 @@ The AWS Entity Resolution Basics scenario executes the following operations. 3. **Start Matching Workflow**: - Description: Initiates a matching workflow to process entity resolution based on predefined configurations. - - The method `listAssetModelProperties` is called to retrieve the property ID values. - - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. There are not - many other useful exceptions for this specific call. If so, display the message and end the program. - -4. **Send data to an AWS IoT SiteWise Asset**: - - Description: This operation sends data to an IoT SiteWise Asset. - - This step uses the method `batchPutAssetPropertyValue`. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. - -5. **Retrieve the value of the IoT SiteWise Asset property**: - - Description: This operation gets data from the asset. - - This step uses the method `getAssetPropertyValue`. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. - -6. **Create an IoT SiteWise Portal**: - - Description: This operation creates an IoT SiteWise portal. - - The method `createPortal` is called. - - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. + - The method `startMatchingJob` is called to start the matching workflow. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + +4. **Get Workflow Job Details**: + - Description: Retrieves details about a specific matching workflow job. + - This step uses the method `getMatchingJob`. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + -7. **Describe the Portal**: - - Description: This operation describes the portal and returns a URL for the portal. - - The method `describePortal` is called and returns the URL. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. +5. **List Matching Workflows**: + - Description: Lists all matching workflows created within the account. + - This step uses the method `listMatchingWorkflows`. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. -8. **Create an IoT SiteWise Gateway**: - - Description: This operation creates an IoT SiteWise Gateway. - - The method `createGateway` is called. - - Exception Handling: Check to see if an `IoTSiteWiseException` is thrown. If so, display the message and end the program. +6. **Get Schema Mapping**: + - Description: Lists all schema mappings available in the account. + - The method `createPortal` is called. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program + +7. **Tag Resource**: + - Description: Adds tags associated with an AWS Entity Resolution resource. + - The method `tagResource` is called. + - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program -9. **Describe the IoT SiteWise Gateway**: - - Description: This operation describes the Gateway. - - The method `describeGateway` is called. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. +8. **Delete Matching Workflow**: + - Description: Deletes a specified matching workflowy. + - The methods `deleteMatchingWorkflow` is called. + - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program -10. **Delete the AWS IoT SiteWise Assets**: - - The `delete` methods are called to clean up the resources. - - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program." ### Program execution -The following shows the output of the AWS IoT SiteWise Basics scenario in the console. +The following shows the output of the AWS Entity Resolution Basics scenario in the console. ``` AWS IoT SiteWise is a fully managed industrial software-as-a-service (SaaS) that @@ -303,26 +295,20 @@ This concludes the AWS SiteWise Scenario ## SOS Tags The following table describes the metadata used in this Basics Scenario. +| action | metadata file | metadata key | +|-------------------------|------------------------|---------------------------------------- | +| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | +| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | +| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | +| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | +| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | +| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | +| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | +| `tagResource ` | entity_metadata.yaml | entity_TagResource | +| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | +| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | +| `scenario` | entity_metadata.yaml | entity_Scenario | -| action | metadata file | metadata key | -|--------------------------------|-----------------------------------|---------------------------------------- | -| `describeGateway` | iot_sitewise_metadata.yaml | iotsitewise_DescribeGateway | -| `deleteGateway ` | iot_sitewise_metadata.yaml | iotsitewise_DeleteGateway | -| `createGateway ` | iot_sitewise_metadata.yaml | iotsitewise_CreateGateway | -| `describePortal` | iot_sitewise_metadata.yaml | iotsitewise_DescribePortal | -| `listAssetModels` | iot_sitewise_metadata.yaml | iotsitewise_ListAssetModels | -| `deletePortal` | iot_sitewise_metadata.yaml | iotsitewise_DeletePortal | -| `createPortal` | iot_sitewise_metadata.yaml | iotsitewise_CreatePortal | -| `deleteAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAssetModel | -| `deleteAsset` | iot_sitewise_metadata.yaml | iotsitewise_DeleteAsset | -| `describeAssetModel` | iot_sitewise_metadata.yaml | iotsitewise_DescribeAssetModel | -| `getAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_GetAssetPropertyValue | -| `batchPutAssetPropertyValue` | iot_sitewise_metadata.yaml | iotsitewise_BatchPutAssetPropertyValue | -| `createAsset` | iot_sitewise_metadata.yaml | iotsitewise_CreateAsset | -| `createAssetModel ` | iot_sitewise_metadata.yaml | iotsitewise_CreateAssetModel | -| `scenario` | iot_sitewise_metadata.yaml | iotsitewise_Scenario | -| `hello` | iot_sitewise_metadata.yaml | iotsitewise_Hello | - From 0d16612110c52d796b9f6914165caff8208fba60 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 13:52:20 -0500 Subject: [PATCH 098/144] updated the Basic Specs --- .../entity/scenario/EntityResActions.java | 39 ++- .../entity/scenario/EntityResScenario.java | 63 +++-- .../basics/entity_resolution/SPECIFICATION.md | 232 +++++++----------- 3 files changed, 162 insertions(+), 172 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index b2dfda3e4f6..260a3bb482b 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -17,36 +17,28 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; -import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; -import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; import software.amazon.awssdk.services.entityresolution.model.OutputSource; -import software.amazon.awssdk.services.entityresolution.model.EntityResolutionException; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; -import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobResponse; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; -import software.amazon.awssdk.services.s3.model.NoSuchKeyException; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; -import java.nio.file.Paths; import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; public class EntityResActions { @@ -318,7 +310,7 @@ public CompletableFuture createMatchingWorkflowAsync(String roleARN, Str return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) .whenComplete((response, exception) -> { if (response != null) { - System.out.println("Workflow created successfully with ID: " + response.workflowArn()); + System.out.println("Workflow created successfully."); } else { throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); } @@ -327,6 +319,30 @@ public CompletableFuture createMatchingWorkflowAsync(String roleARN, Str } // snippet-end:[entityres.java2_create_matching_workflow.main] + // snippet-start:[entityres.java2_tag_resource.main] + /** + * Tags the specified schema mapping ARN. + * + * @param schemaMappingARN the ARN of the schema mapping to tag + */ + public CompletableFuture tagEntityResource(String schemaMappingARN) { + Map tags = new HashMap<>(); + tags.put("tag1", "tag1Value"); + tags.put("tag2", "tag2Value"); + + TagResourceRequest request = TagResourceRequest.builder() + .resourceArn(schemaMappingARN) + .tags(tags) + .build(); + + return getResolutionAsyncClient().tagResource(request) + .thenAccept(response -> System.out.println("Successfully tagged the resource.")) + .exceptionally(exception -> { + throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); + }); + } + + // snippet-end:[entityres.java2_tag_resource.main] /** * Uploads a local file to an Amazon S3 bucket asynchronously. @@ -373,4 +389,5 @@ public boolean doesObjectExist(String bucketName) { } } + } diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 9a6705150ae..83116dc1517 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -4,6 +4,8 @@ package com.example.entity.scenario; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; + import java.util.Map; import java.util.Scanner; import java.util.concurrent.CompletionException; @@ -11,7 +13,8 @@ public class EntityResScenario { public static final String DASHES = new String(new char[80]).replace("\0", "-"); private static final String ROLES_STACK = "EntityResolutionCdkStack"; - public static void main (String[]args) throws InterruptedException { + + public static void main(String[] args) throws InterruptedException { final String usage = """ @@ -26,13 +29,13 @@ public static void main (String[]args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = "MyMatchingWorkflow433"; - String schemaName = "schema232"; + String workflowName = "MyMatchingWorkflow450"; + String schemaName = "schema450"; // Use the AWS CDK to create this AWS resources. See the Readme file. String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; - String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; + String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; EntityResActions actions = new EntityResActions(); @@ -93,33 +96,34 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } ] """; - System.out.println("Upload the JSON to the "+ dataS3bucket +" S3 bucket if it does not exist"); + System.out.println("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); System.out.println(json); waitForInputToContinue(scanner); if (!actions.doesObjectExist(dataS3bucket)) { actions.uploadLocalFileAsync(dataS3bucket, json); } else { - System.out.println("The JSON exists in "+ dataS3bucket); + System.out.println("The JSON exists in " + dataS3bucket); } waitForInputToContinue(scanner); System.out.println(DASHES); System.out.println(DASHES); - System.out.println("Create Schema Mapping"); + System.out.println("1. Create Schema Mapping"); System.out.println(""" Entity Resolution Schema Mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - + In this example, the schema mapping lines up with the fields in the JSON. That is, it contains these fields: id, name, and email. """); waitForInputToContinue(scanner); + String mappingARN = null; try { - actions.createSchemaMappingAsync(schemaName).join(); - System.out.println("Schema mapping was successfully created."); + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); + mappingARN = response.schemaArn(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); System.err.println("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -129,7 +133,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println(DASHES); - System.out.println("Create an AWS Entity Resolution Workflow. "); + System.out.println("2. Create an AWS Entity Resolution Workflow. "); System.out.println(""" An Entity Resolution matching workflow identifies and links records across datasets that represent the same real-world entity, such as @@ -142,7 +146,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); try { String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); - System.out.println("The workflow was successfully created. The ARN is: " + workflowArn); + System.out.println("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); System.err.println("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -150,12 +154,12 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); System.out.println(DASHES); - System.out.println("3. Start the matching job of the " +workflowName +" workflow."); + System.out.println("3. Start the matching job of the " + workflowName + " workflow."); waitForInputToContinue(scanner); - String jobId = null; + String jobId = null; try { jobId = actions.startMatchingJobAsync(workflowName).join(); - System.out.println("The matching job was successfully started. Job ID: " + jobId); + System.out.println("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -164,9 +168,14 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println(DASHES); - System.out.println("4. Get job details."); + System.out.println("4. Get details for job "+jobId); waitForInputToContinue(scanner); - actions.getMatchingJobAsync(jobId, workflowName); + try { + actions.getMatchingJobAsync(jobId, workflowName).join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } System.out.println(DASHES); System.out.println(DASHES); @@ -181,7 +190,24 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println(DASHES); - System.out.println("9. Delete the AWS Entity Resolution Workflow."); + System.out.println("6. List Schema Mappings."); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("7. Tag the "+schemaName +"resource."); + System.out.println(""" + Tags can help you organize and categorize your Entity Resolution resources. + You can also use them to scope user permissions by granting a user permission + to access or change only resources with certain tag values. + In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, + the SchemaMapping is tagged. + """); + actions.tagEntityResource(mappingARN).join(); + waitForInputToContinue(scanner); + System.out.println(DASHES); + + System.out.println(DASHES); + System.out.println("8. Delete the AWS Entity Resolution Workflow."); System.out.println(""" You cannot delete a workflow that is in a running state. Would you like to wait for the workflow to complete. @@ -199,7 +225,6 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Throwable cause = ce.getCause(); System.err.println("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } - // CloudFormationHelper.destroyCloudFormationStack(ROLES_STACK); } waitForInputToContinue(scanner); System.out.println(DASHES); diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 881d0df2e32..0802942e9fd 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -59,24 +59,29 @@ The AWS Entity Resolution Basics scenario executes the following operations. The following shows the output of the AWS Entity Resolution Basics scenario in the console. ``` -AWS IoT SiteWise is a fully managed industrial software-as-a-service (SaaS) that -makes it easy to collect, store, organize, and monitor data from industrial equipment and processes. -It is designed to help industrial and manufacturing organizations collect data from their equipment and -processes, and use that data to make informed decisions about their operations. - -One of the key features of AWS IoT SiteWise is its ability to connect to a wide range of industrial -equipment and systems, including programmable logic controllers (PLCs), sensors, and other -industrial devices. It can collect data from these devices and organize it into a unified data model, -making it easier to analyze and gain insights from the data. AWS IoT SiteWise also provides tools for -visualizing the data, setting up alarms and alerts, and generating reports. - -Another key feature of AWS IoT SiteWise is its ability to scale to handle large volumes of data. -It can collect and store data from thousands of devices and process millions of data points per second, -making it suitable for large-scale industrial operations. Additionally, AWS IoT SiteWise is designed -to be secure and compliant, with features like role-based access controls, data encryption, -and integration with other AWS services for additional security and compliance features. - -Let's get started... +Welcome to the AWS Entity Resolution Scenario. +AWS Entity Resolution is a fully-managed machine learning service provided by +Amazon Web Services (AWS) that helps organizations extract, link, and +organize information from multiple data sources. It leverages natural +language processing and deep learning models to identify and resolve +entities, such as people, places, organizations, and products, +across structured and unstructured data. + +With Entity Resolution, customers can build robust data integration +pipelines to combine and reconcile data from multiple systems, databases, +and documents. The service can handle ambiguous, incomplete, or conflicting +information, and provide a unified view of entities and their relationships. +This can be particularly valuable in applications such as customer 360, +fraud detection, supply chain management, and knowledge management, where +accurate entity identification is crucial. + +The `EntityResolutionAsyncClient` interface in the AWS SDK for Java 2.x +provides a set of methods to programmatically interact with the AWS Entity +Resolution service. This allows developers to automate the entity extraction, +linking, and deduplication process as part of their data processing workflows. +With Entity Resolution, organizations can unlock the value of their data, +improve decision-making, and enhance customer experiences by having a reliable, +comprehensive view of their key entities. Enter 'c' followed by to continue: @@ -84,39 +89,35 @@ c Continuing with the program... -------------------------------------------------------------------------------- -Use AWS CloudFormation to create an IAM role that are required for this scenario. -Stack creation requested, ARN is arn:aws:cloudformation:us-east-1:814548047983:stack/RoleSitewise/29f480c0-75fd-11ef-a42e-12cd4e534049 -Stack created successfully --------------------------------------------------------------------------------- -1. Create an AWS SiteWise Asset Model - An AWS IoT SiteWise Asset Model is a way to represent the physical assets, such as equipment, - processes, and systems, that exist in an industrial environment. This model provides a structured and - hierarchical representation of these assets, allowing users to define the relationships and properties - of each asset. - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -The Asset Model MyAssetModel already exists. The id of the existing model is ffbc475b-73ad-4eb6-bf28-8728818fa8ef. Moving on... - -Enter 'c' followed by to continue: -c -Continuing with the program... - -------------------------------------------------------------------------------- -2. Create an AWS IoT SiteWise Asset - The IoT SiteWise model defines the structure and metadata for your physical assets. Now we - can use the asset model to create the asset. - +Upload the JSON to the glue-5ffb912c3d534e8493bac675c2a3196d S3 bucket if it does not exist +[ + { + "id": "1", + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + { + "id": "2", + "name": "Bob Smith", + "email": "bob.smith@example.com" + }, + { + "id": "3", + "name": "Charlie Black", + "email": "charlie.black@example.com" + } +] Enter 'c' followed by to continue: c Continuing with the program... -Asset created with ID: 4d681624-a303-46dd-8830-6189790ae915 +SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". +SLF4J: Defaulting to no-operation (NOP) logger implementation +SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. +The JSON exists in glue-5ffb912c3d534e8493bac675c2a3196d Enter 'c' followed by to continue: c @@ -124,17 +125,22 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -3. Retrieve the property ID values - To send data to an asset, we need to get the property ID values for the - Temperature and Humidity properties. +1. Create Schema Mapping +Entity Resolution Schema Mapping aligns and integrates data from +multiple sources by identifying and matching corresponding entities +like customers or products. It unifies schemas, resolves conflicts, +and uses machine learning to link related entities, enabling a +consolidated, accurate view for improved data quality and decision-making. + +In this example, the schema mapping lines up with the fields in the JSON. That is, +it contains these fields: id, name, and email. Enter 'c' followed by to continue: c Continuing with the program... -The Humidity property Id is feb4aba6-55f9-4b00-b366-27b9d7e5a747 -The Temperature property Id is 6cb505aa-6bcc-46f4-a12a-7ca5df8eb028 +Schema Mapping Created Successfully! Enter 'c' followed by to continue: c @@ -142,47 +148,36 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -4. Send data to an AWS IoT SiteWise Asset -By sending data to an IoT SiteWise Asset, you can aggregate data from -multiple sources, normalize the data into a standard format, and store it in a -centralized location. This makes it easier to analyze and gain insights from the data. - -This example demonstrate how to generate sample data and ingest it into the AWS IoT SiteWise asset. - +2. Create an AWS Entity Resolution Workflow. +An Entity Resolution matching workflow identifies and links records +across datasets that represent the same real-world entity, such as +customers or products. Using techniques like schema mapping, +data profiling, and machine learning algorithms, +it evaluates attributes like names or emails to detect duplicates +or relationships, even with variations or inconsistencies. +The workflow outputs consolidated, de-duplicated data, Enter 'c' followed by to continue: c Continuing with the program... -Data sent successfully. +Workflow created successfully. +The workflow ARN is: arn:aws:entityresolution:us-east-1:814548047983:matchingworkflow/MyMatchingWorkflow450 Enter 'c' followed by to continue: c Continuing with the program... -------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -5. Retrieve the value of the IoT SiteWise Asset property -IoT SiteWise is an AWS service that allows you to collect, process, and analyze industrial data -from connected equipment and sensors. One of the key benefits of reading an IoT SiteWise property -is the ability to gain valuable insights from your industrial data. - - +3. Start the matching job of the MyMatchingWorkflow450 workflow. Enter 'c' followed by to continue: c Continuing with the program... -The property name is: Temperature property -The value of this property is 23.5 - -Enter 'c' followed by to continue: -c -Continuing with the program... - -The property name is: Humidity property -The value of this property is 65.0 +Job ID: ec2dbd1717624b2b806ed93a04c20049 +The matching job was successfully started. Enter 'c' followed by to continue: c @@ -190,93 +185,48 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -6. Create an IoT SiteWise Portal - An IoT SiteWise Portal allows you to aggregate data from multiple industrial sources, - such as sensors, equipment, and control systems, into a centralized platform. - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Portal created successfully. Portal ID 63e65729-b7a1-410a-aa36-94145fe92153 -The portal Id is 63e65729-b7a1-410a-aa36-94145fe92153 +4. Get details for job ec2dbd1717624b2b806ed93a04c20049 Enter 'c' followed by to continue: c Continuing with the program... +Job status: QUEUED +Job details: GetMatchingJobResponse(JobId=ec2dbd1717624b2b806ed93a04c20049, StartTime=2025-01-30T18:37:57.475Z, Status=QUEUED) -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -7. Describe the Portal - In this step, we will describe the step and provide the portal URL. - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Portal URL: https://p-fy9qnrqy.app.iotsitewise.aws +5. Get Schema Mapping. Enter 'c' followed by to continue: c Continuing with the program... +Attribute Name: id, Attribute Type: UNIQUE_ID +Attribute Name: name, Attribute Type: STRING +Attribute Name: email, Attribute Type: STRING +Schema mapping retrieval completed. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -8. Create an IoTSitewise Gateway -IoTSitewise Gateway serves as the bridge between industrial equipment, sensors, and the -cloud-based IoTSitewise service. It is responsible for securely collecting, processing, and -transmitting data from various industrial assets to the IoTSitewise platform, -enabling real-time monitoring, analysis, and optimization of industrial operations. - - - -Enter 'c' followed by to continue: -c -Continuing with the program... - -The ARN of the gateway is arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 -Gateway creation completed successfully. id is 50320670-1d88-4a7e-9013-1d7e8a3af832 +6. List Schema Mappings. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -9. Describe the IoTSitewise Gateway +7. Tag the schema450resource. +Tags can help you organize and categorize your Entity Resolution resources. +You can also use them to scope user permissions by granting a user permission +to access or change only resources with certain tag values. +In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, +the SchemaMapping is tagged. -Enter 'c' followed by to continue: -c -Continuing with the program... +Successfully tagged the resource. -Gateway Name: myGateway11 -Gateway ARN: arn:aws:iotsitewise:us-east-1:814548047983:gateway/50320670-1d88-4a7e-9013-1d7e8a3af832 -Gateway Platform: GatewayPlatform(GreengrassV2=GreengrassV2(CoreDeviceThingName=myThing78)) -Gateway Creation Date: 2024-09-18T20:34:13.117Z -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -10. Delete the AWS IoT SiteWise Assets -Before you can delete the Asset Model, you must delete the assets. - - -Would you like to delete the IoT Sitewise Assets? (y/n) -y -You selected to delete the Sitewise assets. - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Portal 63e65729-b7a1-410a-aa36-94145fe92153 was deleted successfully. -An unexpected error occurred: Cannot invoke "java.util.concurrent.CompletableFuture.join()" because "future" is null -Asset deleted successfully. -Lets wait 1 min for the asset to be deleted -01:00The Gateway was deleted successfully -00:00Countdown complete! - -Enter 'c' followed by to continue: -c -Continuing with the program... +8. Delete the AWS Entity Resolution Workflow. +You cannot delete a workflow that is in a running state. +Would you like to wait for the workflow to complete. +This can take up to 30 mins (y/n). -Delete the AWS IoT SiteWise Asset Model -Asset model deleted successfully. +n Enter 'c' followed by to continue: c @@ -284,9 +234,7 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -Delete stack requested .... -Stack deleted successfully. -This concludes the AWS SiteWise Scenario +This concludes the AWS Entity Resolution scenario. -------------------------------------------------------------------------------- From 931a61a497ab6cf80240b6d71624839eb01028ae Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 30 Jan 2025 15:51:47 -0500 Subject: [PATCH 099/144] added a readme --- scenarios/basics/entity_resolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 9df7e209432..001e5a6a87b 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -1,5 +1,5 @@ ## Overview -This AWS IoT SiteWise Service basic scenario demonstrates how to interact with the AWS IoT SiteWise service using an AWS SDK. The scenario covers various operations such as creating an Asset Model, creating assets, sending data to assets, and retrieving data. +This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. The scenario covers various operations such as creating a schema mapping, creating a matching workflow, starting a matching job, and so on. ## Key Operations From 01ac1002a29cbb82f1f57da1b4936aac043238a9 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 09:26:55 -0500 Subject: [PATCH 100/144] added additional methods to the scenario --- .../entity/scenario/EntityResActions.java | 26 +++++++++++++++++++ .../entity/scenario/EntityResScenario.java | 1 + 2 files changed, 27 insertions(+) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 260a3bb482b..4a49c8c09af 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -21,12 +21,14 @@ import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; +import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; import software.amazon.awssdk.services.entityresolution.model.OutputSource; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -111,6 +113,30 @@ public static S3AsyncClient getS3AsyncClient() { return s3AsyncClient; } + // snippet-start:[entityres.java2_list_mappings.main] + /** + * Lists the schema mappings associated with the current AWS account. + * This method uses an asynchronous paginator to retrieve the schema mappings, + * and prints the name of each schema mapping to the console. + */ + public void ListSchemaMappings() { + ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() + .build(); + + ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); + + // Iterate through the pages of results + CompletableFuture future = paginator.subscribe(response -> { + response.schemaList().forEach(schemaMapping -> + System.out.println("Schema Mapping Name: " +schemaMapping.schemaName()) + ); + }); + + // Wait for the asynchronous operation to complete + future.join(); + } + // snippet-end:[entityres.java2_list_mappings.main] + // snippet-start:[entityres.java2_delete_matching_workflow.main] /** * Asynchronously deletes a workflow with the specified name. diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 83116dc1517..2df8618317a 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -191,6 +191,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and System.out.println(DASHES); System.out.println("6. List Schema Mappings."); + actions.ListSchemaMappings(); System.out.println(DASHES); System.out.println(DASHES); From 139d2e3ef0d1473f86a5ba87298c6448163482dd Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 09:56:15 -0500 Subject: [PATCH 101/144] added additional methods to the scenario --- .../entity/scenario/EntityResScenario.java | 6 +++-- scenarios/basics/entity_resolution/README.md | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 2df8618317a..5446540f31c 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -29,8 +29,8 @@ public static void main(String[] args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = "MyMatchingWorkflow450"; - String schemaName = "schema450"; + String workflowName = "MyMatchingWorkflow451"; + String schemaName = "schema451"; // Use the AWS CDK to create this AWS resources. See the Readme file. String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; @@ -187,11 +187,13 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { System.err.println("Error retrieving schema mapping: " + ce.getCause().getMessage()); } + waitForInputToContinue(scanner); System.out.println(DASHES); System.out.println(DASHES); System.out.println("6. List Schema Mappings."); actions.ListSchemaMappings(); + waitForInputToContinue(scanner); System.out.println(DASHES); System.out.println(DASHES); diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 001e5a6a87b..4bae9fd6321 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -3,26 +3,24 @@ This AWS Entity Resolution basic scenario demonstrates how to interact with the ## Key Operations -1. **Create an AWS SiteWise Asset Model**: - - This step creates an AWS SiteWise Asset Model by invoking the `createAssetModel` method. +1. **Create an AWS Entity Resolution Schema Mapping**: + - This step creates an AWS Entity Resolution Schema Mapping by invoking the `createSchemaMapping` method. -2. **Create an AWS IoT SiteWise Asset**: - - This operation creates an AWS SiteWise asset. +2. **Create an AWS Entity Resolution Workflow**: + - This step creates an AWS Entity Resolution matching Workflow by invoking the `createMatchingWorkflow` method. -3. **Retrieve the property ID values**: - - To send data to an asset, we need to get the property ID values for the model properties. This scenario uses temperature and humidity properties. +3. **Start Matching Workflow**: + - This step starts the AWS Entity Resolution matching Workflow by invoking the `startMatchingJob` method. -4. **Send data to an AWS IoT SiteWise Asset**: - - This operation sends data to an IoT SiteWise Asset. +4. **Get Workflow Job Details**: + - This step gets workflow job details by `getMatchingJob` method. -5. **Retrieve the value of the IoT SiteWise Asset property**: - - This operation gets data from the asset. **Note** See the Eng spec for a full listing of operations. ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS IoT SiteWise service. The scenario creates this resource using a CloudFormation template. +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. ## Implementations @@ -30,10 +28,10 @@ This scenario example will be implemented in the following languages: - Java - Python -- JavaScript +- Kotlin ## Additional Reading -- [AWS IoT SiteWise Documentation](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/what-is-sitewise.html) +- [AWS Entity Resolution Documentation](https://docs.aws.amazon.com/entityresolution/latest/userguide/what-is-service.html) Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 From c6117d4572c78d59c48f842817a5f5d11bf38754 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 11:04:00 -0500 Subject: [PATCH 102/144] added a Readme for CDK for Entity Resolution --- .../cdk/entityresolution_resources/README.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 resources/cdk/entityresolution_resources/README.md diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md new file mode 100644 index 00000000000..098811142bd --- /dev/null +++ b/resources/cdk/entityresolution_resources/README.md @@ -0,0 +1,106 @@ +# AWS Entity Resolution resources + +## Overview + +Creates the following AWS resources for Amazon DynamoDB item tracker sample applications: + +* An AWS IAM role that has permissions required to run this Scenario. +* An AWS Glue table that provides the input data for the entity resolution matching workflow. +* An Amazon S3 input bucket that is used by the AWS Glue table. +* An Amazon S3 output bucket that is used by the matching workflow to store results of the matching workflow. + +## ⚠️ Important + +* Running this code might result in charges to your AWS account. +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + +## Deploy resources + +You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Interface +(AWS CLI) to deploy and destroy the resources for this example. + +### Deploy with the AWS CDK + +To deploy with the AWS CDK, you must install [Node.js](https://nodejs.org) and the +[AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). + +This example was built and tested with AWS CDK 2.33.0. + +Deploy AWS resources by running the following at a command prompt in this README's folder: + +``` +npm install +cdk deploy +``` + +The stack takes a few minutes to deploy. When it completes, it prints output like +the following: + +``` +Outputs: +doc-example-work-item-tracker-stack.TableName = doc-example-work-item-tracker +``` + +### Deploy with the AWS CLI + +To deploy with the AWS CLI, you must first install the +[AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). + +1. Deploy AWS resources by running the following at a command prompt in this README's folder: + + ``` + aws cloudformation create-stack --template-body file://setup.yaml --stack-name YOUR_STACK_NAME + ``` + + *Note:* The stack name must be unique within an AWS Region and AWS account. You can + specify up to 128 characters, and numbers and hyphens are allowed. + +2. The stack takes a few minutes to deploy. You can check status by running the following: + + ``` + aws cloudformation describe-stacks --stack-name YOUR_STACK_NAME + ``` + + When the stack is ready, it shows `StackStatus` of `CREATE_COMPLETE`. + +3. You can get the outputs from the stack by running the following: + + ``` + aws cloudformation describe-stacks --stack-name STACK_NAME --query Stacks[0].Outputs --output text + ``` + + This results in output like the following: + + ``` + TableName = doc-example-work-item-tracker + ``` + +## Destroy resources + +### Destroy with the AWS CDK + +You can use the AWS CDK to destroy the resources by running the following: + +``` +cdk destroy +``` + +### Destroy with the AWS CLI + +You can use the AWS CLI to destroy the resources by running the following: + +``` +aws cloudformation delete-stack --stack-name YOUR_STACK_NAME +``` + +## Additional resources + +* [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) +* [AWS CLI User Guide for Version 2](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) +* [AWS CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 From 91926f1f7af63c7ef51b11225ad5bf76f7b18429 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 11:22:22 -0500 Subject: [PATCH 103/144] added a Readme for CDK for Entity Resolution --- .../entity/scenario/EntityResScenario.java | 2 +- .../cdk/entityresolution_resources/README.md | 51 +++---------------- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 5446540f31c..d52769dd0dc 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -32,7 +32,7 @@ public static void main(String[] args) throws InterruptedException { String workflowName = "MyMatchingWorkflow451"; String schemaName = "schema451"; - // Use the AWS CDK to create this AWS resources. See the Readme file. + // Use the AWS CDK to create this AWS resources. See the Readme file located at . String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md index 098811142bd..3a1690adba3 100644 --- a/resources/cdk/entityresolution_resources/README.md +++ b/resources/cdk/entityresolution_resources/README.md @@ -21,15 +21,14 @@ You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Inte ### Deploy with the AWS CDK -To deploy with the AWS CDK, you must install [Node.js](https://nodejs.org) and the +To deploy with the AWS CDK, you must install [Java JDK](https://www.oracle.com/ca-en/java/technologies/downloads/) and the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). -This example was built and tested with AWS CDK 2.33.0. +This example was built and tested with AWS CDK 2.135.0. Deploy AWS resources by running the following at a command prompt in this README's folder: ``` -npm install cdk deploy ``` @@ -38,42 +37,12 @@ the following: ``` Outputs: -doc-example-work-item-tracker-stack.TableName = doc-example-work-item-tracker +EntityResolutionCdkStack.EntityResolutionArn = arn:aws:iam::XXXXX:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm +EntityResolutionCdkStack.GlueDataBucketName = glue-XXXXX3196d +EntityResolutionCdkStack.GlueTableArn = arn:aws:glue:us-east-1:XXXXX:table/entity_resolution_db/entity_resolution ``` -### Deploy with the AWS CLI - -To deploy with the AWS CLI, you must first install the -[AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). - -1. Deploy AWS resources by running the following at a command prompt in this README's folder: - - ``` - aws cloudformation create-stack --template-body file://setup.yaml --stack-name YOUR_STACK_NAME - ``` - - *Note:* The stack name must be unique within an AWS Region and AWS account. You can - specify up to 128 characters, and numbers and hyphens are allowed. - -2. The stack takes a few minutes to deploy. You can check status by running the following: - - ``` - aws cloudformation describe-stacks --stack-name YOUR_STACK_NAME - ``` - - When the stack is ready, it shows `StackStatus` of `CREATE_COMPLETE`. - -3. You can get the outputs from the stack by running the following: - - ``` - aws cloudformation describe-stacks --stack-name STACK_NAME --query Stacks[0].Outputs --output text - ``` - - This results in output like the following: - - ``` - TableName = doc-example-work-item-tracker - ``` +Note - Copy these AWS resources into your AWS Entity Resolution scenario. These values are required for the program to successfully run. ## Destroy resources @@ -85,14 +54,6 @@ You can use the AWS CDK to destroy the resources by running the following: cdk destroy ``` -### Destroy with the AWS CLI - -You can use the AWS CLI to destroy the resources by running the following: - -``` -aws cloudformation delete-stack --stack-name YOUR_STACK_NAME -``` - ## Additional resources * [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) From be7cfc05785e420cfc30f0f96d245b50e6d7e466 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 31 Jan 2025 11:32:07 -0500 Subject: [PATCH 104/144] added a source files for CDK --- .../entity/scenario/EntityResScenario.java | 3 +- .../cdk/entityresolution_resources/README.md | 4 +- .../cdk/entityresolution_resources/pom.xml | 60 +++++++++ .../com/myorg/EntityResolutionCdkApp.java | 45 +++++++ .../com/myorg/EntityResolutionCdkStack.java | 117 ++++++++++++++++++ .../com/myorg/EntityResolutionCdkTest.java | 26 ++++ 6 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 resources/cdk/entityresolution_resources/pom.xml create mode 100644 resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java create mode 100644 resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java create mode 100644 resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index d52769dd0dc..3166e662a0c 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -32,7 +32,8 @@ public static void main(String[] args) throws InterruptedException { String workflowName = "MyMatchingWorkflow451"; String schemaName = "schema451"; - // Use the AWS CDK to create this AWS resources. See the Readme file located at . + // Use the AWS CDK to create these AWS resources. + // See the Readme file located at resources/cdk/entityresolution_resources. String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md index 3a1690adba3..262d244b901 100644 --- a/resources/cdk/entityresolution_resources/README.md +++ b/resources/cdk/entityresolution_resources/README.md @@ -2,7 +2,7 @@ ## Overview -Creates the following AWS resources for Amazon DynamoDB item tracker sample applications: +Creates the following AWS resources for the AWS Entity Resolution scenario: * An AWS IAM role that has permissions required to run this Scenario. * An AWS Glue table that provides the input data for the entity resolution matching workflow. @@ -21,7 +21,7 @@ You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Inte ### Deploy with the AWS CDK -To deploy with the AWS CDK, you must install [Java JDK](https://www.oracle.com/ca-en/java/technologies/downloads/) and the +To deploy with the AWS CDK, you must install [Java JDK 17](https://www.oracle.com/ca-en/java/technologies/downloads/) and the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). This example was built and tested with AWS CDK 2.135.0. diff --git a/resources/cdk/entityresolution_resources/pom.xml b/resources/cdk/entityresolution_resources/pom.xml new file mode 100644 index 00000000000..56581afc3e7 --- /dev/null +++ b/resources/cdk/entityresolution_resources/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.myorg + entity_resolution_cdk + 0.1 + + + UTF-8 + 2.135.0 + [10.0.0,11.0.0) + 5.7.1 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.myorg.EntityResolutionCdkApp + + + + + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + + software.constructs + constructs + ${constructs.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java new file mode 100644 index 00000000000..e7428abe1da --- /dev/null +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.Environment; +import software.amazon.awscdk.StackProps; + +import java.util.Arrays; + +public class EntityResolutionCdkApp { + public static void main(final String[] args) { + App app = new App(); + + new EntityResolutionCdkStack(app, "EntityResolutionCdkStack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + .env(Environment.builder() + .account(System.getenv("CDK_DEFAULT_ACCOUNT")) + .region(System.getenv("CDK_DEFAULT_REGION")) + .build()) + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + .env(Environment.builder() + .account("123456789012") + .region("us-east-1") + .build()) + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + .build()); + + app.synth(); + } +} + diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java new file mode 100644 index 00000000000..4aecc53409a --- /dev/null +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -0,0 +1,117 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.myorg; + +import software.amazon.awscdk.*; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awscdk.services.s3.*; +import software.amazon.awscdk.services.glue.*; +import software.constructs.Construct; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class EntityResolutionCdkStack extends Stack { + public EntityResolutionCdkStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public EntityResolutionCdkStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + // 1. Create an S3 bucket for the Glue Data Table + String uniqueId = UUID.randomUUID().toString().replace("-", ""); // Remove dashes to ensure compatibility + Bucket glueDataBucket = Bucket.Builder.create(this, "GlueDataBucket") + .bucketName("glue-" + uniqueId) + .versioned(true) + .build(); + + // 2. Create a Glue database + CfnDatabase glueDatabase = CfnDatabase.Builder.create(this, "GlueDatabase") + .catalogId(this.getAccount()) + .databaseInput(CfnDatabase.DatabaseInputProperty.builder() + .name("entity_resolution_db") + .build()) + .build(); + + // 3. Create a Glue table referencing the S3 bucket + CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") + .catalogId(this.getAccount()) + .databaseName(glueDatabase.getRef()) // Ensure Glue Table references the database correctly + .tableInput(CfnTable.TableInputProperty.builder() + .name("entity_resolution") // Fixed table name reference + .tableType("EXTERNAL_TABLE") + .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() + .columns(List.of( + CfnTable.ColumnProperty.builder().name("id").type("string").build(), // Fixed: id is a string, + CfnTable.ColumnProperty.builder().name("name").type("string").build(), + CfnTable.ColumnProperty.builder().name("email").type("string").build() + )) + .location("s3://" + glueDataBucket.getBucketName() + "/data/") // Append subpath for data + .inputFormat("org.apache.hadoop.mapred.TextInputFormat") + .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") + .serdeInfo(CfnTable.SerdeInfoProperty.builder() + .serializationLibrary("org.openx.data.jsonserde.JsonSerDe") // Set JSON SerDe + .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON + .build()) + .build()) + .build()) + .build(); + + // Ensure Glue Table is created after the Database + glueTable.addDependency(glueDatabase); + + // 4. Create an IAM Role for AWS Entity Resolution + Role entityResolutionRole = Role.Builder.create(this, "EntityResolutionRole") + .assumedBy(new ServicePrincipal("entityresolution.amazonaws.com")) // AWS Entity Resolution assumes this role + .managedPolicies(List.of( + ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"), + ManagedPolicy.fromAwsManagedPolicyName("AWSEntityResolutionConsoleFullAccess"), + ManagedPolicy.fromAwsManagedPolicyName("AWSGlueConsoleFullAccess"), + ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSGlueServiceRole") + )) + .build(); + + // Add custom permissions for Entity Resolution + entityResolutionRole.addToPolicy(PolicyStatement.Builder.create() + .actions(List.of( + "entityresolution:StartMatchingWorkflow", + "entityresolution:GetMatchingWorkflow" + )) + .resources(List.of("*")) // Adjust permissions if needed + .build()); + + // 5. Create an S3 bucket for output data + Bucket outputBucket = Bucket.Builder.create(this, "OutputBucket") + .bucketName("entity-resolution-output-" + id.toLowerCase()) + .versioned(true) + .build(); + + // 6. Output the Role ARN + new CfnOutput(this, "EntityResolutionArn", CfnOutputProps.builder() + .value(entityResolutionRole.getRoleArn()) + .description("The ARN of the Glue Role") + .build()); + + // 7. Construct and output the Glue Table ARN + String glueTableArn = String.format("arn:aws:glue:%s:%s:table/%s/%s", + this.getRegion(), // Region where the stack is deployed + this.getAccount(), // AWS account ID + glueDatabase.getRef(), // Glue database name (resolved reference) + "entity_resolution" // Corrected table name + ); + + new CfnOutput(this, "GlueTableArn", CfnOutputProps.builder() + .value(glueTableArn) + .description("The ARN of the Glue Table") + .build()); + + // 8. Output the name of the Glue Data Bucket + new CfnOutput(this, "GlueDataBucketName", CfnOutputProps.builder() + .value(glueDataBucket.getBucketName()) // Outputs the bucket name + .description("The name of the Glue Data Bucket") + .build()); + } +} diff --git a/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java b/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java new file mode 100644 index 00000000000..ac832c96bdb --- /dev/null +++ b/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java @@ -0,0 +1,26 @@ +// package com.myorg; + +// import software.amazon.awscdk.App; +// import software.amazon.awscdk.assertions.Template; +// import java.io.IOException; + +// import java.util.HashMap; + +// import org.junit.jupiter.api.Test; + +// example test. To run these tests, uncomment this file, along with the +// example resource in java/src/main/java/com/myorg/EntityResolutionCdkStack.java +// public class EntityResolutionCdkTest { + +// @Test +// public void testStack() throws IOException { +// App app = new App(); +// EntityResolutionCdkStack stack = new EntityResolutionCdkStack(app, "test"); + +// Template template = Template.fromStack(stack); + +// template.hasResourceProperties("AWS::SQS::Queue", new HashMap() {{ +// put("VisibilityTimeout", 300); +// }}); +// } +// } From 8a8d61a774f108152ce5e1b6b9cecca47f1e34d1 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 3 Feb 2025 12:20:21 -0500 Subject: [PATCH 105/144] added tests --- .../src/test/java/EntityResTests.java | 150 ++++++++++++++++++ scenarios/basics/entity_resolution/README.md | 2 +- 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 javav2/example_code/entityresolution/src/test/java/EntityResTests.java diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java new file mode 100644 index 00000000000..113d9a44bbc --- /dev/null +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -0,0 +1,150 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + + +import com.example.entity.scenario.EntityResActions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class EntityResTests { + + private static String workflowName = ""; + private static String schemaName = ""; + + private static String roleARN = ""; + private static String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + private static String outputBucket = ""; + private static String inputGlueTableArn = ""; + + private static String mappingARN = ""; + + private static String jobId = ""; + + private static String workflowArn =""; + private static EntityResActions actions = new EntityResActions(); + @BeforeAll + public static void setUp() { + workflowName = "MyMatchingWorkflow456"; + schemaName = "schema456"; + roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; + dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; + inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + + String json = """ + [ + { + "id": "1", + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + { + "id": "2", + "name": "Bob Smith", + "email": "bob.smith@example.com" + }, + { + "id": "3", + "name": "Charlie Black", + "email": "charlie.black@example.com" + } + ] + """; + if (!actions.doesObjectExist(dataS3bucket)) { + actions.uploadLocalFileAsync(dataS3bucket, json); + } else { + System.out.println("The JSON exists in " + dataS3bucket); + } + } + + @Test + @Tag("IntegrationTest") + @Order(1) + public void testCreateMapping() { + assertDoesNotThrow(() -> { + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); + mappingARN = response.schemaArn(); + assertNotNull(mappingARN); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(2) + public void testCreateMappingWorkflow() { + assertDoesNotThrow(() -> { + workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + assertNotNull(workflowArn); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(3) + public void testStartWorkflow() { + assertDoesNotThrow(() -> { + jobId = actions.startMatchingJobAsync(workflowName).join(); + assertNotNull(workflowArn); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(4) + public void testGetJobDetails() { + assertDoesNotThrow(() -> { + actions.getMatchingJobAsync(jobId, workflowName).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(5) + public void testtSchemaMappingDetails() { + assertDoesNotThrow(() -> { + actions.getSchemaMappingAsync(schemaName).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(6) + public void testListSchemaMappings() { + assertDoesNotThrow(() -> { + actions.ListSchemaMappings(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(7) + public void testLTagResources() { + assertDoesNotThrow(() -> { + actions.tagEntityResource(mappingARN).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(8) + public void testLDeleteMapping() { + assertDoesNotThrow(() -> { + System.out.println("Wait 30 mins for the workflow to complete"); + Thread.sleep(1800000); + actions.deleteMatchingWorkflowAsync(workflowName).join(); + }); + } + + +} diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 4bae9fd6321..6099ece90f5 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -20,7 +20,7 @@ This AWS Entity Resolution basic scenario demonstrates how to interact with the ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. See the Readme file at resources/cdk/entityresolution_resources. ## Implementations From 1b8fdbd04d0fa6db481fc2a79d7ed1bdc79c3184 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 5 Feb 2025 11:43:04 -0500 Subject: [PATCH 106/144] modified the tests --- javav2/example_code/entityresolution/pom.xml | 9 +++ .../entity/scenario/EntityResScenario.java | 16 ++-- .../src/test/java/EntityResTests.java | 80 ++++++++++++++++--- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index f710bd02f78..7332e236ab5 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -82,10 +82,19 @@ software.amazon.awssdk netty-nio-client + + org.slf4j + slf4j-api + 2.0.13 + software.amazon.awssdk cloudformation + + org.apache.logging.log4j + log4j-slf4j2-impl + \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 3166e662a0c..ff426a2fa5d 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -12,8 +12,6 @@ public class EntityResScenario { public static final String DASHES = new String(new char[80]).replace("\0", "-"); - private static final String ROLES_STACK = "EntityResolutionCdkStack"; - public static void main(String[] args) throws InterruptedException { final String usage = """ @@ -29,19 +27,19 @@ public static void main(String[] args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = "MyMatchingWorkflow451"; - String schemaName = "schema451"; + String workflowName = args[0]; + String schemaName = args[1]; // Use the AWS CDK to create these AWS resources. // See the Readme file located at resources/cdk/entityresolution_resources. - String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; - String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; - String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; - String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + String roleARN = args[2]; + String dataS3bucket = args[3]; + String outputBucket = args[4]; + String inputGlueTableArn = args[5]; EntityResActions actions = new EntityResActions(); Scanner scanner = new Scanner(System.in); - System.out.println("Welcome to the AWS Entity Resolution Scenario. "); + System.out.println("Welcome to the AWS Entity Resolution Scenario."); System.out.println(""" AWS Entity Resolution is a fully-managed machine learning service provided by Amazon Web Services (AWS) that helps organizations extract, link, and diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index 113d9a44bbc..76f6844b1ab 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -3,27 +3,37 @@ import com.example.entity.scenario.EntityResActions; +import com.google.gson.Gson; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; - +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import java.util.Random; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @TestInstance(TestInstance.Lifecycle.PER_METHOD) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class EntityResTests { - + private static final Logger logger = LoggerFactory.getLogger(EntityResTests.class); private static String workflowName = ""; private static String schemaName = ""; private static String roleARN = ""; - private static String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; + private static String dataS3bucket = ""; private static String outputBucket = ""; private static String inputGlueTableArn = ""; @@ -35,12 +45,17 @@ public class EntityResTests { private static EntityResActions actions = new EntityResActions(); @BeforeAll public static void setUp() { - workflowName = "MyMatchingWorkflow456"; - schemaName = "schema456"; - roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm"; - dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d"; - outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack"; - inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution"; + Random random = new Random(); + int randomValue = random.nextInt(10000) + 1; + workflowName = "MyMatchingWorkflow"+randomValue; + schemaName = "schema"+randomValue; + Gson gson = new Gson(); + String jsonVal = getSecretValues(); + SecretValues values = gson.fromJson(jsonVal, SecretValues.class); + roleARN = values.getRoleARN(); + dataS3bucket = values.getDataS3bucket(); + outputBucket = values.getOutputBucket(); + inputGlueTableArn = values.getInputGlueTableArn(); String json = """ [ @@ -77,6 +92,7 @@ public void testCreateMapping() { mappingARN = response.schemaArn(); assertNotNull(mappingARN); }); + logger.info("Test 1 passed"); } @Test @@ -87,6 +103,7 @@ public void testCreateMappingWorkflow() { workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); assertNotNull(workflowArn); }); + logger.info("Test 2 passed"); } @Test @@ -97,6 +114,7 @@ public void testStartWorkflow() { jobId = actions.startMatchingJobAsync(workflowName).join(); assertNotNull(workflowArn); }); + logger.info("Test 3 passed"); } @Test @@ -106,6 +124,7 @@ public void testGetJobDetails() { assertDoesNotThrow(() -> { actions.getMatchingJobAsync(jobId, workflowName).join(); }); + logger.info("Test 4 passed"); } @Test @@ -115,6 +134,7 @@ public void testtSchemaMappingDetails() { assertDoesNotThrow(() -> { actions.getSchemaMappingAsync(schemaName).join(); }); + logger.info("Test 5 passed"); } @Test @@ -124,6 +144,7 @@ public void testListSchemaMappings() { assertDoesNotThrow(() -> { actions.ListSchemaMappings(); }); + logger.info("Test 6 passed"); } @Test @@ -133,6 +154,7 @@ public void testLTagResources() { assertDoesNotThrow(() -> { actions.tagEntityResource(mappingARN).join(); }); + logger.info("Test 7 passed"); } @Test @@ -144,7 +166,47 @@ public void testLDeleteMapping() { Thread.sleep(1800000); actions.deleteMatchingWorkflowAsync(workflowName).join(); }); + logger.info("Test 8 passed"); + } + + private static String getSecretValues() { + SecretsManagerClient secretClient = SecretsManagerClient.builder() + .region(Region.US_EAST_1) + .build(); + String secretName = "test/entity"; + + GetSecretValueRequest valueRequest = GetSecretValueRequest.builder() + .secretId(secretName) + .build(); + + GetSecretValueResponse valueResponse = secretClient.getSecretValue(valueRequest); + return valueResponse.secretString(); } + @Nested + @DisplayName("A class used to get test values from test/cognito (an AWS Secrets Manager secret)") + class SecretValues { + private String roleARN; + private String dataS3bucket; + + private String outputBucket; + + private String inputGlueTableArn; + public String getRoleARN() { + return roleARN; + } + + public String getDataS3bucket() { + return dataS3bucket; + } + + public String getOutputBucket() { + return outputBucket; + } + + public String getInputGlueTableArn() { + return inputGlueTableArn; + } + } } From aa158557138bcc237802b819f02a148b4933ff17 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 5 Feb 2025 13:38:31 -0500 Subject: [PATCH 107/144] modified the tests --- javav2/example_code/entityresolution/pom.xml | 22 ++- .../entity/scenario/EntityResActions.java | 30 ++-- .../entity/scenario/EntityResScenario.java | 135 +++++++++--------- .../src/main/resources/log4j2.xml | 18 +++ javav2/example_code/iotsitewise/pom.xml | 8 +- 5 files changed, 124 insertions(+), 89 deletions(-) create mode 100644 javav2/example_code/entityresolution/src/main/resources/log4j2.xml diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index 7332e236ab5..0e9154ddd6f 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -35,6 +35,13 @@ pom import + + org.apache.logging.log4j + log4j-bom + 2.23.1 + pom + import + @@ -82,19 +89,26 @@ software.amazon.awssdk netty-nio-client + + software.amazon.awssdk + cloudformation + + + org.apache.logging.log4j + log4j-core + org.slf4j slf4j-api 2.0.13 - software.amazon.awssdk - cloudformation + org.apache.logging.log4j + log4j-slf4j2-impl org.apache.logging.log4j - log4j-slf4j2-impl + log4j-1.2-api - \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 4a49c8c09af..b2273c2226a 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -9,9 +9,9 @@ import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.entityresolution.EntityResolutionClient; import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowRequest; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowResponse; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; @@ -35,7 +35,6 @@ import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; - import java.time.Duration; import java.util.HashMap; import java.util.List; @@ -43,8 +42,7 @@ import java.util.concurrent.CompletableFuture; public class EntityResActions { - - private EntityResolutionClient resolutionClient; + private static final Logger logger = LoggerFactory.getLogger(EntityResActions.class); private static EntityResolutionAsyncClient entityResolutionAsyncClient; @@ -128,7 +126,7 @@ public void ListSchemaMappings() { // Iterate through the pages of results CompletableFuture future = paginator.subscribe(response -> { response.schemaList().forEach(schemaMapping -> - System.out.println("Schema Mapping Name: " +schemaMapping.schemaName()) + logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) ); }); @@ -182,7 +180,7 @@ public CompletableFuture createSchemaMappingAsync(S return getResolutionAsyncClient().createSchemaMapping(request) .whenComplete((response, exception) -> { if (response != null) { - System.out.println("Schema Mapping Created Successfully!"); + logger.info("Schema Mapping Created Successfully!"); } else { throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); } @@ -207,7 +205,7 @@ public CompletableFuture getSchemaMappingAsync(String .whenComplete((response, exception) -> { if (response != null) { response.mappedInputFields().forEach(attribute -> - System.out.println("Attribute Name: " + attribute.fieldName() + + logger.info("Attribute Name: " + attribute.fieldName() + ", Attribute Type: " + attribute.type().toString())); } else { throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); @@ -232,8 +230,8 @@ public CompletableFuture getMatchingJobAsync(String jobId, String workflow return getResolutionAsyncClient().getMatchingJob(request) .thenAccept(response -> { - System.out.println("Job status: " + response.status()); - System.out.println("Job details: " + response.toString()); + logger.info("Job status: " + response.status()); + logger.info("Job details: " + response.toString()); }) .exceptionally(ex -> { throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); @@ -258,7 +256,7 @@ public CompletableFuture startMatchingJobAsync(String workflowName) { if (response != null) { // Get the job ID from the response String jobId = response.jobId(); - System.out.println("Job ID: " + jobId); + logger.info("Job ID: " + jobId); } else { // Handle the exception if the response is null throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); @@ -284,11 +282,11 @@ public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, return getResolutionAsyncClient().getMatchingJob(request) .thenApply(response -> { - System.out.println("\nJob status: " + response.status()); + logger.info("\nJob status: " + response.status()); return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); }) .exceptionally(exception -> { - System.out.println("Error checking workflow status: " + exception.getMessage()); + logger.info("Error checking workflow status: " + exception.getMessage()); return false; }); } @@ -336,7 +334,7 @@ public CompletableFuture createMatchingWorkflowAsync(String roleARN, Str return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) .whenComplete((response, exception) -> { if (response != null) { - System.out.println("Workflow created successfully."); + logger.info("Workflow created successfully."); } else { throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); } @@ -362,7 +360,7 @@ public CompletableFuture tagEntityResource(String schemaMappingARN) { .build(); return getResolutionAsyncClient().tagResource(request) - .thenAccept(response -> System.out.println("Successfully tagged the resource.")) + .thenAccept(response -> logger.info("Successfully tagged the resource.")) .exceptionally(exception -> { throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); }); @@ -414,6 +412,4 @@ public boolean doesObjectExist(String bucketName) { return false; } } - - } diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index ff426a2fa5d..a5598894800 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -6,11 +6,13 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; -import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Scanner; import java.util.concurrent.CompletionException; public class EntityResScenario { + private static final Logger logger = LoggerFactory.getLogger(EntityResScenario.class); public static final String DASHES = new String(new char[80]).replace("\0", "-"); public static void main(String[] args) throws InterruptedException { @@ -27,20 +29,26 @@ public static void main(String[] args) throws InterruptedException { outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. """; - String workflowName = args[0]; - String schemaName = args[1]; + + // if (args.length != 6) { + // logger.info(usage); + // return; + // } + + String workflowName = "workflow100" ; //args[0]; + String schemaName = "schemaName100" ;//args[1]; // Use the AWS CDK to create these AWS resources. // See the Readme file located at resources/cdk/entityresolution_resources. - String roleARN = args[2]; - String dataS3bucket = args[3]; - String outputBucket = args[4]; - String inputGlueTableArn = args[5]; + String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm" ; //args[2]; + String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d" ; //args[3]; + String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; //args[4]; + String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution" ; //args[5]; EntityResActions actions = new EntityResActions(); Scanner scanner = new Scanner(System.in); - System.out.println("Welcome to the AWS Entity Resolution Scenario."); - System.out.println(""" + logger.info("Welcome to the AWS Entity Resolution Scenario."); + logger.info(""" AWS Entity Resolution is a fully-managed machine learning service provided by Amazon Web Services (AWS) that helps organizations extract, link, and organize information from multiple data sources. It leverages natural @@ -66,10 +74,9 @@ Amazon Web Services (AWS) that helps organizations extract, link, and """); waitForInputToContinue(scanner); - System.out.println(DASHES); - - System.out.println(DASHES); + logger.info(DASHES); + logger.info(DASHES); /* This JSON is a valid input for the AWS Entity Resolution service. The JSON represents an array of three objects, each containing an "id", "name", and "email" @@ -95,20 +102,20 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } ] """; - System.out.println("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); - System.out.println(json); + logger.info("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); + logger.info(json); waitForInputToContinue(scanner); if (!actions.doesObjectExist(dataS3bucket)) { actions.uploadLocalFileAsync(dataS3bucket, json); } else { - System.out.println("The JSON exists in " + dataS3bucket); + logger.info("The JSON exists in " + dataS3bucket); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("1. Create Schema Mapping"); - System.out.println(""" + logger.info(DASHES); + logger.info("1. Create Schema Mapping"); + logger.info(""" Entity Resolution Schema Mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, @@ -125,15 +132,15 @@ Amazon Web Services (AWS) that helps organizations extract, link, and mappingARN = response.schemaArn(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("2. Create an AWS Entity Resolution Workflow. "); - System.out.println(""" + logger.info(DASHES); + logger.info("2. Create an AWS Entity Resolution Workflow. "); + logger.info(""" An Entity Resolution matching workflow identifies and links records across datasets that represent the same real-world entity, such as customers or products. Using techniques like schema mapping, @@ -145,59 +152,59 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); try { String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); - System.out.println("The workflow ARN is: " + workflowArn); + logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); - System.out.println(DASHES); - System.out.println("3. Start the matching job of the " + workflowName + " workflow."); + logger.info(DASHES); + logger.info("3. Start the matching job of the " + workflowName + " workflow."); waitForInputToContinue(scanner); String jobId = null; try { jobId = actions.startMatchingJobAsync(workflowName).join(); - System.out.println("The matching job was successfully started."); + logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("4. Get details for job "+jobId); + logger.info(DASHES); + logger.info("4. Get details for job "+jobId); waitForInputToContinue(scanner); try { actions.getMatchingJobAsync(jobId, workflowName).join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); } - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("5. Get Schema Mapping."); + logger.info(DASHES); + logger.info("5. Get Schema Mapping."); waitForInputToContinue(scanner); try { actions.getSchemaMappingAsync(schemaName).join(); - System.out.println("Schema mapping retrieval completed."); + logger.info("Schema mapping retrieval completed."); } catch (CompletionException ce) { - System.err.println("Error retrieving schema mapping: " + ce.getCause().getMessage()); + logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("6. List Schema Mappings."); + logger.info(DASHES); + logger.info("6. List Schema Mappings."); actions.ListSchemaMappings(); waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("7. Tag the "+schemaName +"resource."); - System.out.println(""" + logger.info(DASHES); + logger.info("7. Tag the "+schemaName +"resource."); + logger.info(""" Tags can help you organize and categorize your Entity Resolution resources. You can also use them to scope user permissions by granting a user permission to access or change only resources with certain tag values. @@ -206,51 +213,51 @@ Amazon Web Services (AWS) that helps organizations extract, link, and """); actions.tagEntityResource(mappingARN).join(); waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("8. Delete the AWS Entity Resolution Workflow."); - System.out.println(""" + logger.info(DASHES); + logger.info("8. Delete the AWS Entity Resolution Workflow."); + logger.info(""" You cannot delete a workflow that is in a running state. Would you like to wait for the workflow to complete. This can take up to 30 mins (y/n). """); String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { - System.out.println("You selected to delete Entity Resolution Workflow."); + logger.info("You selected to delete Entity Resolution Workflow."); waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); try { actions.deleteMatchingWorkflowAsync(workflowName).join(); - System.out.println("Workflow deleted successfully!"); + logger.info("Workflow deleted successfully!"); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - System.err.println("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } } waitForInputToContinue(scanner); - System.out.println(DASHES); + logger.info(DASHES); - System.out.println(DASHES); - System.out.println("This concludes the AWS Entity Resolution scenario."); - System.out.println(DASHES); + logger.info(DASHES); + logger.info("This concludes the AWS Entity Resolution scenario."); + logger.info(DASHES); } private static void waitForInputToContinue(Scanner scanner) { while (true) { - System.out.println(""); - System.out.println("Enter 'c' followed by to continue:"); + logger.info(""); + logger.info("Enter 'c' followed by to continue:"); String input = scanner.nextLine(); if (input.trim().equalsIgnoreCase("c")) { - System.out.println("Continuing with the program..."); - System.out.println(""); + logger.info("Continuing with the program..."); + logger.info(""); break; } else { // Handle invalid input. - System.out.println("Invalid input. Please try again."); + logger.info("Invalid input. Please try again."); } } } @@ -272,8 +279,8 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota // Check workflow status every 60 seconds if (secondsElapsed % 60 == 0 || remainingTime <= 0) { if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { - System.out.println(); // Move to the next line after countdown - System.out.println("Countdown complete: Workflow is in SUCCEEDED state!"); + logger.info(""); // Move to the next line after countdown + logger.info("Countdown complete: Workflow is in SUCCEEDED state!"); break; } } diff --git a/javav2/example_code/entityresolution/src/main/resources/log4j2.xml b/javav2/example_code/entityresolution/src/main/resources/log4j2.xml new file mode 100644 index 00000000000..225afe2b3a8 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/javav2/example_code/iotsitewise/pom.xml b/javav2/example_code/iotsitewise/pom.xml index ce7da2101c6..e98a1f632d7 100644 --- a/javav2/example_code/iotsitewise/pom.xml +++ b/javav2/example_code/iotsitewise/pom.xml @@ -82,6 +82,10 @@ software.amazon.awssdk ssooidc + + software.amazon.awssdk + cloudformation + org.apache.logging.log4j log4j-core @@ -91,10 +95,6 @@ slf4j-api 2.0.13 - - software.amazon.awssdk - cloudformation - org.apache.logging.log4j log4j-slf4j2-impl From 98377fc41395a836782fb1e193f757ac2f90108b Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 5 Feb 2025 14:54:44 -0500 Subject: [PATCH 108/144] modified the tests --- .../entityresolution/src/test/java/EntityResTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index 76f6844b1ab..5e915b8f0ad 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -162,7 +162,7 @@ public void testLTagResources() { @Order(8) public void testLDeleteMapping() { assertDoesNotThrow(() -> { - System.out.println("Wait 30 mins for the workflow to complete"); + logger.info("Wait 30 mins for the workflow to complete"); Thread.sleep(1800000); actions.deleteMatchingWorkflowAsync(workflowName).join(); }); From 2f783cecbeeb7857b253106e8dfee0d6a3de18d8 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 6 Feb 2025 09:34:20 -0500 Subject: [PATCH 109/144] added new SOS Yaml file --- .doc_gen/metadata/entityresolution_metadata.yaml | 12 ++++++++++++ .../example/entity/scenario/EntityResScenario.java | 2 -- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .doc_gen/metadata/entityresolution_metadata.yaml diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml new file mode 100644 index 00000000000..971641b7df2 --- /dev/null +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -0,0 +1,12 @@ +entityresolution_CreateSchemaMapping: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_create_schema.main + services: + entityresolution: {CreateSchemaMapping} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index a5598894800..cacb3763921 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -241,8 +241,6 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info("This concludes the AWS Entity Resolution scenario."); logger.info(DASHES); - - } private static void waitForInputToContinue(Scanner scanner) { From 435e3fd3716c832ad01ba5caa70bbde71681d3d5 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 6 Feb 2025 11:47:05 -0500 Subject: [PATCH 110/144] updated a comment --- .../main/java/com/example/entity/scenario/EntityResActions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index b2273c2226a..1a605fe11f1 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -162,7 +162,7 @@ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) /** * Creates a schema mapping asynchronously. * - * @param schemaName the name of the schema to create the mapping for + * @param schemaName the name of the schema to create * @return a {@link CompletableFuture} that represents the asynchronous creation of the schema mapping */ public CompletableFuture createSchemaMappingAsync(String schemaName) { From 3abbf616d4dec605b801552693726375cb4ff0b9 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 6 Feb 2025 14:22:25 -0500 Subject: [PATCH 111/144] updated the YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 971641b7df2..400cd855972 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -1,3 +1,15 @@ +entityresolution_GetSchemaMapping: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_get_schema_mapping.main + services: + entityresolution: {GetSchemaMapping} entityresolution_CreateSchemaMapping: languages: Java: From 0ac11b53a9dcdc583d2ec6f02cc1fab8d4f8bd83 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 09:45:04 -0500 Subject: [PATCH 112/144] updated the YAML file --- .../metadata/entityresolution_metadata.yaml | 96 +++++++++++++++++++ .../example/entity/HelloEntityResoultion.java | 79 +++++++++++++++ .../entity/scenario/EntityResActions.java | 1 - 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 400cd855972..f2a04ec572d 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -1,3 +1,99 @@ +entityresolution_HelloEntity: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_tag_resource.main + services: + entityresolution: {HelloEntity} +entityresolution_TagEntityResource: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_tag_resource.main + services: + entityresolution: {TagEntityResource} +entityresolution_CreateMatchingWork: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_create_matching_workflow.main + services: + entityresolution: {CreateMatchingWork} +entityresolution_CheckWorkflowStatus: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_check_matching_workflow.main + services: + entityresolution: {CheckWorkflowStatus} +entityresolution_StartMatchingJob: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_start_job.main + services: + entityresolution: {StartMatchingJob} +entityresolution_GetMatchingJob: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_get_job.main + services: + entityresolution: {GetMatchingJob} +entityresolution_DeleteMatchingWorkflow: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_delete_matching_workflow.main + services: + entityresolution: {DeleteMatchingWorkflow} +entityresolution_ListSchemaMappings: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_list_mappings.main + services: + entityresolution: {ListSchemaMappings} entityresolution_GetSchemaMapping: languages: Java: diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java new file mode 100644 index 00000000000..3ed975dd6b0 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity; + +import com.example.entity.scenario.EntityResScenario; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; +import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; +import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class HelloEntityResoultion { + + private static final Logger logger = LoggerFactory.getLogger(HelloEntityResoultion.class); + + private static EntityResolutionAsyncClient entityResolutionAsyncClient; + public static void main(String[] args) { + + + } + + public static EntityResolutionAsyncClient getResolutionAsyncClient() { + if (entityResolutionAsyncClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return entityResolutionAsyncClient; + } + + + public void ListSchemaMappings() { + ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() + .build(); + + ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); + + // Iterate through the pages of results + CompletableFuture future = paginator.subscribe(response -> { + response.schemaList().forEach(schemaMapping -> + logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) + ); + }); + + // Wait for the asynchronous operation to complete + future.join(); + } +} diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 1a605fe11f1..e4185ea01cc 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -79,7 +79,6 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { return entityResolutionAsyncClient; } - public static S3AsyncClient getS3AsyncClient() { if (s3AsyncClient == null) { /* From a90584b9314c8acedc97dda4eb57937ddd2b6540 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 11:15:05 -0500 Subject: [PATCH 113/144] updated Hello example --- .../metadata/entityresolution_metadata.yaml | 10 +++++-- .../example/entity/HelloEntityResoultion.java | 29 ++++++++++++------- .../basics/entity_resolution/SPECIFICATION.md | 2 +- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index f2a04ec572d..5a7a6279b00 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -1,4 +1,8 @@ -entityresolution_HelloEntity: +entityresolution_Hello: + title: Hello &ERlong; + title_abbrev: Hello &ER; + synopsis: get started using &ER;. + category: Hello languages: Java: versions: @@ -7,9 +11,9 @@ entityresolution_HelloEntity: excerpts: - description: snippet_tags: - - entityres.java2_tag_resource.main + - entityres.java2_hello.main services: - entityresolution: {HelloEntity} + entityresolution: {listMatchingWorkflows} entityresolution_TagEntityResource: languages: Java: diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java index 3ed975dd6b0..78c57af049b 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java @@ -12,20 +12,28 @@ import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; +import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowResponse; +import software.amazon.awssdk.services.entityresolution.model.ListIdMappingJobsRequest; +import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsRequest; +import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsResponse; import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; +import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; +import software.amazon.awssdk.services.entityresolution.paginators.ListIdMappingJobsPublisher; +import software.amazon.awssdk.services.entityresolution.paginators.ListMatchingWorkflowsPublisher; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import java.time.Duration; import java.util.concurrent.CompletableFuture; +// snippet-start:[entityres.java2_hello.main] public class HelloEntityResoultion { private static final Logger logger = LoggerFactory.getLogger(HelloEntityResoultion.class); private static EntityResolutionAsyncClient entityResolutionAsyncClient; public static void main(String[] args) { - - + listMatchingWorkflows(); } public static EntityResolutionAsyncClient getResolutionAsyncClient() { @@ -57,19 +65,19 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { .build(); } return entityResolutionAsyncClient; - } + } - public void ListSchemaMappings() { - ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() - .build(); + public static void listMatchingWorkflows() { + ListMatchingWorkflowsRequest request = ListMatchingWorkflowsRequest.builder().build(); - ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); + ListMatchingWorkflowsPublisher paginator = + getResolutionAsyncClient().listMatchingWorkflowsPaginator(request); - // Iterate through the pages of results + // Iterate through the paginated results asynchronously CompletableFuture future = paginator.subscribe(response -> { - response.schemaList().forEach(schemaMapping -> - logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) + response.workflowSummaries().forEach(workflow -> + logger.info("Matching Workflow Name: " + workflow.workflowName()) ); }); @@ -77,3 +85,4 @@ public void ListSchemaMappings() { future.join(); } } +// snippet-end:[entityres.java2_hello.main] diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 0802942e9fd..990a899296a 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -7,7 +7,7 @@ This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database and a table, and two S3 buckets. A CDK script is provided to create these resources. ## Hello AWS Entity Resolution -This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listIdMappingJobsPaginator` to demonstrate how you can read through workflow job information. +This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listMatchingWorkflowsPaginator` to demonstrate how you can read through workflow information. ## Basics Scenario Program Flow The AWS Entity Resolution Basics scenario executes the following operations. From eaba9d31c12af3faa43ddb7e904d076b29fc0c4c Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 12:39:48 -0500 Subject: [PATCH 114/144] updated Hello example --- .../metadata/entityresolution_metadata.yaml | 29 ++++++++++++++++++- .../example/entity/HelloEntityResoultion.java | 28 +++++++++++------- .../entity/scenario/EntityResActions.java | 2 ++ .../entity/scenario/EntityResScenario.java | 5 ++-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 5a7a6279b00..a456a378044 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -121,4 +121,31 @@ entityresolution_CreateSchemaMapping: snippet_tags: - entityres.java2_create_schema.main services: - entityresolution: {CreateSchemaMapping} \ No newline at end of file + entityresolution: {CreateSchemaMapping} +entityresolution_Scenario: + synopsis_list: + - Create Schema Mapping. + - Create an &ERlong; workflow. + - Start the matching job for the workflow. + - Get details for the matching job. + - Get Schema Mapping. + - List all Schema Mappings. + - Tag the Schema Mapping resource. + - Delete the AWS Entity Resolution Workflow. + - Delete the &ERlong; Assets. + category: Basics + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + sdkguide: + excerpts: + - description: Run an interactive scenario demonstrating &ERlong; features. + snippet_tags: + - entityres.java2_scenario.main + - description: A wrapper class for &ITSW; SDK methods. + snippet_tags: + - iotsitewise.java2.actions.main + services: + iotsitewise: {} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java index 78c57af049b..f5dcbc3aeec 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/HelloEntityResoultion.java @@ -3,7 +3,6 @@ package com.example.entity; -import com.example.entity.scenario.EntityResScenario; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; @@ -12,21 +11,20 @@ import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; -import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowRequest; -import software.amazon.awssdk.services.entityresolution.model.GetIdMappingWorkflowResponse; -import software.amazon.awssdk.services.entityresolution.model.ListIdMappingJobsRequest; import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsRequest; -import software.amazon.awssdk.services.entityresolution.model.ListMatchingWorkflowsResponse; -import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; -import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; -import software.amazon.awssdk.services.entityresolution.paginators.ListIdMappingJobsPublisher; import software.amazon.awssdk.services.entityresolution.paginators.ListMatchingWorkflowsPublisher; -import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; - import java.time.Duration; import java.util.concurrent.CompletableFuture; // snippet-start:[entityres.java2_hello.main] +/** + * Before running this Java V2 code example, set up your development + * environment, including your credentials. + * + * For more information, see the following documentation topic: + * + * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html + */ public class HelloEntityResoultion { private static final Logger logger = LoggerFactory.getLogger(HelloEntityResoultion.class); @@ -65,9 +63,17 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { .build(); } return entityResolutionAsyncClient; - } + /** + * Lists all matching workflows using an asynchronous paginator. + *

+ * This method requests a paginated list of matching workflows from the + * AWS Entity Resolution service and logs the names of the retrieved workflows. + * It uses an asynchronous approach with a paginator and waits for the operation + * to complete using {@code CompletableFuture#join()}. + *

+ */ public static void listMatchingWorkflows() { ListMatchingWorkflowsRequest request = ListMatchingWorkflowsRequest.builder().build(); diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index e4185ea01cc..2c3cbb3691f 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +// snippet-start:[entityres.java2_actions.main] public class EntityResActions { private static final Logger logger = LoggerFactory.getLogger(EntityResActions.class); @@ -412,3 +413,4 @@ public boolean doesObjectExist(String bucketName) { } } } +// snippet-end:[entityres.java2_actions.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index cacb3763921..6099da2d4c5 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -11,6 +11,7 @@ import java.util.Scanner; import java.util.concurrent.CompletionException; +// snippet-start:[entityres.java2_scenario.main] public class EntityResScenario { private static final Logger logger = LoggerFactory.getLogger(EntityResScenario.class); public static final String DASHES = new String(new char[80]).replace("\0", "-"); @@ -289,5 +290,5 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } } - -} \ No newline at end of file +} +// snippet-end:[entityres.java2_scenario.main] \ No newline at end of file From 0b7450b2a3de5f17df4da0c3c1c27d3653e3ab49 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 13:06:18 -0500 Subject: [PATCH 115/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index a456a378044..eeeaacfc20b 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -131,7 +131,7 @@ entityresolution_Scenario: - Get Schema Mapping. - List all Schema Mappings. - Tag the Schema Mapping resource. - - Delete the AWS Entity Resolution Workflow. + - Delete the Workflow. - Delete the &ERlong; Assets. category: Basics languages: From c7aea305544f1ad3931ecbd1eff24cd45fc515ab Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 13:15:06 -0500 Subject: [PATCH 116/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index eeeaacfc20b..98fa364530d 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -26,7 +26,7 @@ entityresolution_TagEntityResource: - entityres.java2_tag_resource.main services: entityresolution: {TagEntityResource} -entityresolution_CreateMatchingWork: +entityresolution_CreateMatchingWorkflow: languages: Java: versions: @@ -37,7 +37,7 @@ entityresolution_CreateMatchingWork: snippet_tags: - entityres.java2_create_matching_workflow.main services: - entityresolution: {CreateMatchingWork} + entityresolution: {CreateMatchingWorkflow} entityresolution_CheckWorkflowStatus: languages: Java: From afc92d2b520c867092e4b8834d4c396e0b25cbf5 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 13:22:01 -0500 Subject: [PATCH 117/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 98fa364530d..e9212ca8c64 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -131,7 +131,6 @@ entityresolution_Scenario: - Get Schema Mapping. - List all Schema Mappings. - Tag the Schema Mapping resource. - - Delete the Workflow. - Delete the &ERlong; Assets. category: Basics languages: From 50cc703164b9df5c9a202bbfab87b5eeb38e06e5 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 14:19:12 -0500 Subject: [PATCH 118/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index e9212ca8c64..d5f3ea8749d 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -147,4 +147,4 @@ entityresolution_Scenario: snippet_tags: - iotsitewise.java2.actions.main services: - iotsitewise: {} \ No newline at end of file + entityresolution: {} \ No newline at end of file From 7c2308735b4f1df2f31cdc62b1700106e8751d6d Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 14:31:45 -0500 Subject: [PATCH 119/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index d5f3ea8749d..79ba21a6022 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -143,7 +143,7 @@ entityresolution_Scenario: - description: Run an interactive scenario demonstrating &ERlong; features. snippet_tags: - entityres.java2_scenario.main - - description: A wrapper class for &ITSW; SDK methods. + - description: A wrapper class for &ERlong; SDK methods. snippet_tags: - iotsitewise.java2.actions.main services: From 4d6489d7430f688002d0c56389b4f91d6da3c029 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 7 Feb 2025 15:02:58 -0500 Subject: [PATCH 120/144] updated YAML file --- .doc_gen/metadata/entityresolution_metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 79ba21a6022..c9c23f58645 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -145,6 +145,6 @@ entityresolution_Scenario: - entityres.java2_scenario.main - description: A wrapper class for &ERlong; SDK methods. snippet_tags: - - iotsitewise.java2.actions.main + - entityres.java2_actions.main services: entityresolution: {} \ No newline at end of file From ddd692c764037d9486f97c71aabd2b92f6d2e032 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 18 Feb 2025 11:36:12 -0500 Subject: [PATCH 121/144] updated POM --- javav2/example_code/entityresolution/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index 0e9154ddd6f..d2fee1f06c4 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -7,7 +7,6 @@ org.example entityresolution 1.0-SNAPSHOT - UTF-8 17 From 8c26561b7e9887b664f0e56514161730b3671750 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 24 Feb 2025 15:27:24 -0500 Subject: [PATCH 122/144] Applied review changes --- javav2/example_code/entityresolution/pom.xml | 8 + .../entity/scenario/CloudFormationHelper.java | 188 ++++++++ .../entity/scenario/EntityResActions.java | 423 ++++++++++-------- .../entity/scenario/EntityResScenario.java | 196 +++++--- .../src/main/resources/TODO.md | 8 + .../src/main/resources/data.csv | 5 + .../src/main/resources/data.json | 3 + .../resources/{glue.yaml => template.yaml} | 107 +++-- .../src/test/java/EntityResTests.java | 3 +- .../cdk/entityresolution_resources/README.md | 48 +- .../cdk/entityresolution_resources/cdk.json | 68 +++ .../com/myorg/EntityResolutionCdkStack.java | 126 ++++-- scenarios/basics/entity_resolution/README.md | 22 +- .../basics/entity_resolution/SPECIFICATION.md | 123 +++-- 14 files changed, 908 insertions(+), 420 deletions(-) create mode 100644 javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java create mode 100644 javav2/example_code/entityresolution/src/main/resources/TODO.md create mode 100644 javav2/example_code/entityresolution/src/main/resources/data.csv create mode 100644 javav2/example_code/entityresolution/src/main/resources/data.json rename javav2/example_code/entityresolution/src/main/resources/{glue.yaml => template.yaml} (72%) create mode 100644 resources/cdk/entityresolution_resources/cdk.json diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index d2fee1f06c4..19684620c48 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -92,6 +92,14 @@ software.amazon.awssdk cloudformation
+ + software.amazon.awssdk + sso + + + software.amazon.awssdk + ssooidc + org.apache.logging.log4j log4j-core diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java new file mode 100644 index 00000000000..9de189ea437 --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java @@ -0,0 +1,188 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.entity.scenario; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient; +import software.amazon.awssdk.services.cloudformation.model.Capability; +import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse; +import software.amazon.awssdk.services.cloudformation.model.Output; +import software.amazon.awssdk.services.cloudformation.model.Stack; +import software.amazon.awssdk.services.cloudformation.waiters.CloudFormationAsyncWaiter; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class CloudFormationHelper { + private static final String CFN_TEMPLATE = "template.yaml"; + private static final Logger logger = LoggerFactory.getLogger(CloudFormationHelper.class); + + private static CloudFormationAsyncClient cloudFormationClient; + + public static void main(String[] args) { + emptyS3Bucket(args[0]); + } + + private static CloudFormationAsyncClient getCloudFormationClient() { + if (cloudFormationClient == null) { + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .connectionTimeout(Duration.ofSeconds(60)) + .readTimeout(Duration.ofSeconds(60)) + .writeTimeout(Duration.ofSeconds(60)) + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) + .apiCallAttemptTimeout(Duration.ofSeconds(90)) + .retryStrategy(RetryMode.STANDARD) + .build(); + + cloudFormationClient = CloudFormationAsyncClient.builder() + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return cloudFormationClient; + } + + public static void deployCloudFormationStack(String stackName) { + String templateBody; + boolean doesExist = describeStack(stackName); + if (!doesExist) { + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Path filePath = Paths.get(classLoader.getResource(CFN_TEMPLATE).toURI()); + templateBody = Files.readString(filePath); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + + getCloudFormationClient().createStack(b -> b.stackName(stackName) + .templateBody(templateBody) + .capabilities(Capability.CAPABILITY_IAM)) + .whenComplete((csr, t) -> { + if (csr != null) { + System.out.println("Stack creation requested, ARN is " + csr.stackId()); + try (CloudFormationAsyncWaiter waiter = getCloudFormationClient().waiter()) { + waiter.waitUntilStackCreateComplete(request -> request.stackName(stackName)) + .whenComplete((dsr, th) -> { + if (th != null) { + System.out.println("Error waiting for stack creation: " + th.getMessage()); + } else { + dsr.matched().response().orElseThrow(() -> new RuntimeException("Failed to deploy")); + System.out.println("Stack created successfully"); + } + }).join(); + } + } else { + System.out.format("Error creating stack: " + t.getMessage(), t); + throw new RuntimeException(t.getCause().getMessage(), t); + } + }).join(); + } else { + logger.info("{} stack already exists", CFN_TEMPLATE); + } + } + + // Check to see if the Stack exists before deploying it + public static Boolean describeStack(String stackName) { + try { + CompletableFuture future = getCloudFormationClient().describeStacks(); + DescribeStacksResponse stacksResponse = (DescribeStacksResponse) future.join(); + List stacks = stacksResponse.stacks(); + for (Stack myStack : stacks) { + if (myStack.stackName().compareTo(stackName) == 0) { + return true; + } + } + } catch (CloudFormationException e) { + System.err.println(e.getMessage()); + } + return false; + } + + public static void destroyCloudFormationStack(String stackName) { + getCloudFormationClient().deleteStack(b -> b.stackName(stackName)) + .whenComplete((dsr, t) -> { + if (dsr != null) { + System.out.println("Delete stack requested ...."); + try (CloudFormationAsyncWaiter waiter = getCloudFormationClient().waiter()) { + waiter.waitUntilStackDeleteComplete(request -> request.stackName(stackName)) + .whenComplete((waiterResponse, throwable) -> + System.out.println("Stack deleted successfully.")) + .join(); + } + } else { + System.out.format("Error deleting stack: " + t.getMessage(), t); + throw new RuntimeException(t.getCause().getMessage(), t); + } + }).join(); + } + + public static CompletableFuture> getStackOutputsAsync(String stackName) { + CloudFormationAsyncClient cloudFormationAsyncClient = getCloudFormationClient(); + + DescribeStacksRequest describeStacksRequest = DescribeStacksRequest.builder() + .stackName(stackName) + .build(); + + return cloudFormationAsyncClient.describeStacks(describeStacksRequest) + .handle((describeStacksResponse, throwable) -> { + if (throwable != null) { + throw new RuntimeException("Failed to get stack outputs for: " + stackName, throwable); + } + + // Process the result + if (describeStacksResponse.stacks().isEmpty()) { + throw new RuntimeException("Stack not found: " + stackName); + } + + Stack stack = describeStacksResponse.stacks().get(0); + Map outputs = new HashMap<>(); + for (Output output : stack.outputs()) { + outputs.put(output.outputKey(), output.outputValue()); + } + + return outputs; + }); + } + + public static void emptyS3Bucket(String bucketName) { + S3AsyncClient s3Client = S3AsyncClient.builder().build(); + + s3Client.listObjectsV2(req -> req.bucket(bucketName)) + .thenCompose(response -> { + List> deleteFutures = response.contents().stream() + .map(s3Object -> s3Client.deleteObject(req -> req + .bucket(bucketName) + .key(s3Object.key()))) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(deleteFutures.toArray(new CompletableFuture[0])); + }) + .join(); + + s3Client.close(); + } +} + diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 2c3cbb3691f..7876abfcf46 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -18,28 +18,30 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; +import software.amazon.awssdk.services.entityresolution.model.JobMetrics; import software.amazon.awssdk.services.entityresolution.model.ListSchemaMappingsRequest; import software.amazon.awssdk.services.entityresolution.model.OutputAttribute; import software.amazon.awssdk.services.entityresolution.model.OutputSource; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; +import software.amazon.awssdk.services.entityresolution.model.SchemaAttributeType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { @@ -59,23 +61,23 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return entityResolutionAsyncClient; } @@ -90,43 +92,43 @@ public static S3AsyncClient getS3AsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); s3AsyncClient = S3AsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return s3AsyncClient; } // snippet-start:[entityres.java2_list_mappings.main] + /** - * Lists the schema mappings associated with the current AWS account. - * This method uses an asynchronous paginator to retrieve the schema mappings, - * and prints the name of each schema mapping to the console. + * Lists the schema mappings associated with the current AWS account. This method uses an asynchronous paginator to + * retrieve the schema mappings, and prints the name of each schema mapping to the console. */ public void ListSchemaMappings() { ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() - .build(); + .build(); ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); - // Iterate through the pages of results + // Iterate through the pages of results CompletableFuture future = paginator.subscribe(response -> { response.schemaList().forEach(schemaMapping -> - logger.info("Schema Mapping Name: " +schemaMapping.schemaName()) + logger.info("Schema Mapping Name: " + schemaMapping.schemaName()) ); }); @@ -136,6 +138,7 @@ public void ListSchemaMappings() { // snippet-end:[entityres.java2_list_mappings.main] // snippet-start:[entityres.java2_delete_matching_workflow.main] + /** * Asynchronously deletes a workflow with the specified name. * @@ -145,20 +148,21 @@ public void ListSchemaMappings() { */ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { DeleteMatchingWorkflowRequest request = DeleteMatchingWorkflowRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().deleteMatchingWorkflow(request) - .thenAccept(response -> { - // No response object, just log success - }) - .exceptionally(exception -> { - throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); - }); + .thenAccept(response -> { + // No response object, just log success + }) + .exceptionally(exception -> { + throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); + }); } // snippet-end:[entityres.java2_delete_matching_workflow.main] // snippet-start:[entityres.java2_create_schema.main] + /** * Creates a schema mapping asynchronously. * @@ -166,184 +170,231 @@ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) * @return a {@link CompletableFuture} that represents the asynchronous creation of the schema mapping */ public CompletableFuture createSchemaMappingAsync(String schemaName) { - List schemaAttributes = List.of( - SchemaInputAttribute.builder().matchKey("id").fieldName("id").type("UNIQUE_ID").build(), - SchemaInputAttribute.builder().matchKey("name").fieldName("name").type("STRING").build(), - SchemaInputAttribute.builder().matchKey("email").fieldName("email").type("STRING").build() - ); + List schemaAttributes = null; + if (schemaName.startsWith("json")) { + schemaAttributes = List.of( + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build() + ); + } else { + schemaAttributes = List.of( + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build(), + SchemaInputAttribute.builder().fieldName("phone").type(SchemaAttributeType.PROVIDER_ID).subType("STRING").build() + ); + } CreateSchemaMappingRequest request = CreateSchemaMappingRequest.builder() - .schemaName(schemaName) - .mappedInputFields(schemaAttributes) - .build(); + .schemaName(schemaName) + .mappedInputFields(schemaAttributes) + .build(); return getResolutionAsyncClient().createSchemaMapping(request) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("Schema Mapping Created Successfully!"); - } else { - throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); - } - }); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("[{}] schema mapping Created Successfully!", schemaName); + } else { + throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_create_schema.main] // snippet-start:[entityres.java2_get_schema_mapping.main] + /** * Retrieves the schema mapping asynchronously. * * @param schemaName the name of the schema to retrieve the mapping for - * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation is complete + * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation + * is complete * @throws RuntimeException if the schema mapping retrieval fails */ public CompletableFuture getSchemaMappingAsync(String schemaName) { GetSchemaMappingRequest mappingRequest = GetSchemaMappingRequest.builder() - .schemaName(schemaName) - .build(); + .schemaName(schemaName) + .build(); return getResolutionAsyncClient().getSchemaMapping(mappingRequest) - .whenComplete((response, exception) -> { - if (response != null) { - response.mappedInputFields().forEach(attribute -> - logger.info("Attribute Name: " + attribute.fieldName() + - ", Attribute Type: " + attribute.type().toString())); - } else { - throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); - } - }); + .whenComplete((response, exception) -> { + if (response != null) { + response.mappedInputFields().forEach(attribute -> + logger.info("Attribute Name: " + attribute.fieldName() + + ", Attribute Type: " + attribute.type().toString())); + } else { + throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_get_schema_mapping.main] // snippet-start:[entityres.java2_get_job.main] + /** * Asynchronously retrieves a matching job based on the provided job ID and workflow name. * - * @param jobId the ID of the job to retrieve - * @param workflowName the name of the workflow associated with the job + * @param jobId the ID of the job to retrieve + * @param workflowName the name of the workflow associated with the job * @return a {@link CompletableFuture} that completes when the job information is available or an exception occurs */ public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenAccept(response -> { - logger.info("Job status: " + response.status()); - logger.info("Job details: " + response.toString()); - }) - .exceptionally(ex -> { - throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); - }); + .thenAccept(response -> { + logger.info("Job status: " + response.status()); + logger.info("Job details: " + response.toString()); + }) + .exceptionally(ex -> { + throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); + }); } // snippet-end:[entityres.java2_get_job.main] // snippet-start:[entityres.java2_start_job.main] + /** * Starts a matching job asynchronously for the specified workflow name. * * @param workflowName the name of the workflow for which to start the matching job - * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty string if the operation fails + * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty + * string if the operation fails */ public CompletableFuture startMatchingJobAsync(String workflowName) { StartMatchingJobRequest jobRequest = StartMatchingJobRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().startMatchingJob(jobRequest) - .whenComplete((response, exception) -> { - if (response != null) { - // Get the job ID from the response - String jobId = response.jobId(); - logger.info("Job ID: " + jobId); - } else { - // Handle the exception if the response is null - throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); - } - }) - .thenApply(response -> response != null ? response.jobId() : ""); + .whenComplete((response, exception) -> { + if (response != null) { + // Get the job ID from the response + String jobId = response.jobId(); + logger.info("Job ID: " + jobId); + } else { + // Handle the exception if the response is null + throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response != null ? response.jobId() : ""); } // snippet-end:[entityres.java2_start_job.main] // snippet-start:[entityres.java2_check_matching_workflow.main] + /** * Checks the status of a workflow asynchronously. * - * @param jobId the ID of the job to check - * @param workflowName the name of the workflow to check - * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed successfully + * @param jobId the ID of the job to check + * @param workflowName the name of the workflow to check + * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed + * successfully */ public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenApply(response -> { - logger.info("\nJob status: " + response.status()); - return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); - }) - .exceptionally(exception -> { - logger.info("Error checking workflow status: " + exception.getMessage()); - return false; - }); + .thenApply(response -> { + logger.info("\nJob status: " + response.status()); + return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); + }) + .exceptionally(exception -> { + logger.info("Error checking workflow status: " + exception.getMessage()); + return false; + }); } // snippet-end:[entityres.java2_check_matching_workflow.main] // snippet-start:[entityres.java2_create_matching_workflow.main] + /** * Creates an asynchronous CompletableFuture to manage the creation of a matching workflow. * * @param roleARN the AWS IAM role ARN to be used for the workflow execution * @param workflowName the name of the workflow to be created * @param outputBucket the S3 bucket path where the workflow output will be stored - * @param inputGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source - * @param schemaName the name of the schema to be used for the input source + * @param jsonGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source + * @param jsonErSchemaMappingName the name of the schema to be used for the input source * @return a CompletableFuture that, when completed, will return the ARN of the created workflow */ - public CompletableFuture createMatchingWorkflowAsync(String roleARN, String workflowName, String outputBucket, String inputGlueTableArn, String schemaName) { - InputSource inputSource = InputSource.builder() - .inputSourceARN(inputGlueTableArn) - .schemaName(schemaName) - .build(); + public CompletableFuture createMatchingWorkflowAsync( + String roleARN + , String workflowName + , String outputBucket + , String jsonGlueTableArn + , String jsonErSchemaMappingName + , String csvGlueTableArn + , String csvErSchemaMappingName) { + + InputSource jsonInputSource = InputSource.builder() + .inputSourceARN(jsonGlueTableArn) + .schemaName(jsonErSchemaMappingName) + .applyNormalization(false) + .build(); + + InputSource csvInputSource = InputSource.builder() + .inputSourceARN(csvGlueTableArn) + .schemaName(csvErSchemaMappingName) + .applyNormalization(false) + .build(); - OutputAttribute outputAttribute = OutputAttribute.builder() - .name("id") - .build(); + OutputAttribute idOutputAttribute = OutputAttribute.builder() + .name("id") + .build(); + + OutputAttribute nameOutputAttribute = OutputAttribute.builder() + .name("name") + .build(); + + OutputAttribute emailOutputAttribute = OutputAttribute.builder() + .name("email") + .build(); + + OutputAttribute phoneOutputAttribute = OutputAttribute.builder() + .name("phone") + .build(); OutputSource outputSource = OutputSource.builder() - .outputS3Path(outputBucket) - .output(outputAttribute) - .build(); + .outputS3Path("s3://" + outputBucket + "/eroutput") + .output(idOutputAttribute, nameOutputAttribute, emailOutputAttribute, phoneOutputAttribute) + .applyNormalization(false) + .build(); - ResolutionTechniques type = ResolutionTechniques.builder() - .resolutionType(ResolutionType.ML_MATCHING) - .build(); + ResolutionTechniques resolutionType = ResolutionTechniques.builder() + .resolutionType(ResolutionType.ML_MATCHING) + .build(); CreateMatchingWorkflowRequest workflowRequest = CreateMatchingWorkflowRequest.builder() - .roleArn(roleARN) - .description("Created by using the AWS SDK for Java") - .workflowName(workflowName) - .inputSourceConfig(List.of(inputSource)) - .outputSourceConfig(List.of(outputSource)) - .resolutionTechniques(type) - .build(); + .roleArn(roleARN) + .description("Created by using the AWS SDK for Java") + .workflowName(workflowName) + .inputSourceConfig(List.of(jsonInputSource, csvInputSource)) + .outputSourceConfig(List.of(outputSource)) + .resolutionTechniques(resolutionType) + .build(); return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("Workflow created successfully."); - } else { - throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); - } - }) - .thenApply(CreateMatchingWorkflowResponse::workflowArn); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Workflow created successfully."); + } else { + throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); + } + }) + .thenApply(CreateMatchingWorkflowResponse::workflowArn); } // snippet-end:[entityres.java2_create_matching_workflow.main] // snippet-start:[entityres.java2_tag_resource.main] + /** * Tags the specified schema mapping ARN. * @@ -355,62 +406,62 @@ public CompletableFuture tagEntityResource(String schemaMappingARN) { tags.put("tag2", "tag2Value"); TagResourceRequest request = TagResourceRequest.builder() - .resourceArn(schemaMappingARN) - .tags(tags) - .build(); + .resourceArn(schemaMappingARN) + .tags(tags) + .build(); return getResolutionAsyncClient().tagResource(request) - .thenAccept(response -> logger.info("Successfully tagged the resource.")) - .exceptionally(exception -> { - throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); - }); + .thenAccept(response -> logger.info("Successfully tagged the resource.")) + .exceptionally(exception -> { + throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); + }); } - // snippet-end:[entityres.java2_tag_resource.main] + public CompletableFuture getJobInfo(String workflowName, String jobId){ + return getResolutionAsyncClient().getMatchingJob(b -> b + .workflowName(workflowName) + .jobId(jobId)) + .thenApply(response -> response.metrics()); + + } /** - * Uploads a local file to an Amazon S3 bucket asynchronously. + * Uploads data to an Amazon S3 bucket asynchronously. * - * @param bucketName the name of the S3 bucket to upload the file to - * @param json the JSON data to be uploaded - * @return a {@link CompletableFuture} representing the asynchronous operation of uploading the file + * @param bucketName the name of the S3 bucket to upload the data to + * @param jsonData the JSON data to be uploaded + * @param csvData the CSV data to be uploaded + * @return a {@link CompletableFuture} representing both asynchronous operation of uploading the data * @throws RuntimeException if an error occurs during the file upload */ - public CompletableFuture uploadLocalFileAsync(String bucketName, String json) { - - String key = "data/data.json"; // Corrected: No leading "/" - PutObjectRequest objectRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(key) - .contentType("application/json") - .build(); - - CompletableFuture response = getS3AsyncClient().putObject(objectRequest, AsyncRequestBody.fromString(json)); - return response.whenComplete((resp, ex) -> { - if (ex != null) { - throw new RuntimeException("Failed to upload file", ex); - } - }); - } - /** - * Checks if a specific object exists in an Amazon S3 bucket. - * - * @param bucketName the name of the S3 bucket to check - * @return true if the object exists, false otherwise - */ - public boolean doesObjectExist(String bucketName) { - try { - String key = "data/data.json"; - getS3AsyncClient().headObject(HeadObjectRequest.builder() + public void uploadInputData(String bucketName, String jsonData, String csvData) { + // Upload JSON data. + String jsonKey = "jsonData/data.json"; + PutObjectRequest jsonUploadRequest = PutObjectRequest.builder() .bucket(bucketName) - .key(key) - .build()); - return true; // File exists + .key(jsonKey) + .contentType("application/json") + .build(); + + CompletableFuture jsonUploadResponse = getS3AsyncClient().putObject(jsonUploadRequest, AsyncRequestBody.fromString(jsonData)); + + // Upload CSV data. + String csvKey = "csvData/data.csv"; + PutObjectRequest csvUploadRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(csvKey) + .contentType("text/csv") + .build(); + CompletableFuture csvUploadResponse = getS3AsyncClient().putObject(csvUploadRequest, AsyncRequestBody.fromString(csvData)); + + CompletableFuture.allOf(jsonUploadResponse, csvUploadResponse) + .whenComplete((result, ex) -> { + if (ex != null) { + throw new RuntimeException("Failed to upload files", ex); + } + }).join(); - } catch (S3Exception e) { - return false; - } } -} -// snippet-end:[entityres.java2_actions.main] \ No newline at end of file +// snippet-end:[entityres.java2_actions.main] +} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 6099da2d4c5..a0551a628a9 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -8,43 +8,34 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.JobMetrics; + +import java.util.Map; import java.util.Scanner; +import java.util.UUID; import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_scenario.main] public class EntityResScenario { private static final Logger logger = LoggerFactory.getLogger(EntityResScenario.class); public static final String DASHES = new String(new char[80]).replace("\0", "-"); - public static void main(String[] args) throws InterruptedException { - - final String usage = """ - - Usage: - + private static final String STACK_NAME = "EntityResolutionCdkStack"; + private static final String ENTITY_RESOLUTION_ROLE_ARN_KEY = "EntityResolutionRoleArn"; + private static final String GLUE_DATA_BUCKET_NAME_KEY = "GlueDataBucketName"; + private static final String JSON_GLUE_TABLE_ARN_KEY = "JsonErGlueTableArn"; + private static final String CSV_GLUE_TABLE_ARN_KEY = "CsvErGlueTableArn"; + private static String glueBucketName; + private static String workflowName = "workflow-"+ UUID.randomUUID(); - Where: - workflowName - A unique identifier for the matching workflow, used in the entity resolution process. - schemaName - The name of the schema, which defines the structure and attributes for the data being processed. - roleARN: The ARN of the IAM role, that grants permissions for the entity resolution workflow (this resource is created using the CDK script. See the Readme). - dataS3bucket: The S3 bucket,that stores the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. - outputBucket: The S3 bucket URL where the results of the entity resolution workflow are stored (this resource is created using the CDK script. See the Readme).. - inputGlueTableArn: The ARN of the AWS Glue table which provides the input data for the entity resolution process (this resource is created using the CDK script. See the Readme).. - """; - - // if (args.length != 6) { - // logger.info(usage); - // return; - // } - - String workflowName = "workflow100" ; //args[0]; - String schemaName = "schemaName100" ;//args[1]; + public static void main(String[] args) throws InterruptedException { - // Use the AWS CDK to create these AWS resources. - // See the Readme file located at resources/cdk/entityresolution_resources. - String roleARN = "arn:aws:iam::814548047983:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm" ; //args[2]; - String dataS3bucket = "glue-5ffb912c3d534e8493bac675c2a3196d" ; //args[3]; - String outputBucket = "s3://entity-resolution-output-entityresolutioncdkstack" ; //args[4]; - String inputGlueTableArn = "arn:aws:glue:us-east-1:814548047983:table/entity_resolution_db/entity_resolution" ; //args[5]; + String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); + String jsonSchemaMappingArn = null; + String csvSchemaMappingName = "csv-" + UUID.randomUUID(); + String csvSchemaMappingArn = null; + String roleARN; + String csvGlueTableArn; + String jsonGlueTableArn; EntityResActions actions = new EntityResActions(); Scanner scanner = new Scanner(System.in); @@ -78,6 +69,26 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); + logger.info(""" + To prepare the AWS resources needed for this scenario application, the next step uploads + a CloudFormation template whose resulting stack creates the following resources: + - An AWS Glue Data Catalog table + - An AWS IAM role + - An AWS S3 bucket + - An AWS Entity Resolution Schema + + It can take a couple minutes for the Stack to finish creating the resources. + """); + waitForInputToContinue(scanner); + logger.info("Generating resources..."); + CloudFormationHelper.deployCloudFormationStack(STACK_NAME); + Map outputsMap = CloudFormationHelper.getStackOutputsAsync(STACK_NAME).join(); + roleARN = outputsMap.get(ENTITY_RESOLUTION_ROLE_ARN_KEY); + glueBucketName = outputsMap.get(GLUE_DATA_BUCKET_NAME_KEY); + csvGlueTableArn = outputsMap.get(CSV_GLUE_TABLE_ARN_KEY); + jsonGlueTableArn = outputsMap.get(JSON_GLUE_TABLE_ARN_KEY); + logger.info(DASHES); + waitForInputToContinue(scanner); /* This JSON is a valid input for the AWS Entity Resolution service. The JSON represents an array of three objects, each containing an "id", "name", and "email" @@ -85,57 +96,57 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Entity Resolution service. */ String json = """ - [ - { - "id": "1", - "name": "Alice Johnson", - "email": "alice.johnson@example.com" - }, - { - "id": "2", - "name": "Bob Smith", - "email": "bob.smith@example.com" - }, - { - "id": "3", - "name": "Charlie Black", - "email": "charlie.black@example.com" - } - ] - """; - logger.info("Upload the JSON to the " + dataS3bucket + " S3 bucket if it does not exist"); + {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} + {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} + {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} + """; + logger.info("Upload the following JSON objects to the {} S3 bucket.", glueBucketName); logger.info(json); + String csv = """ + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + """; + logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); + logger.info(csv); waitForInputToContinue(scanner); - if (!actions.doesObjectExist(dataS3bucket)) { - actions.uploadLocalFileAsync(dataS3bucket, json); - } else { - logger.info("The JSON exists in " + dataS3bucket); - } + actions.uploadInputData(glueBucketName, json, csv); + logger.info("The JSON objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("1. Create Schema Mapping"); logger.info(""" - Entity Resolution Schema Mapping aligns and integrates data from + Entity Resolution schema mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - In this example, the schema mapping lines up with the fields in the JSON. That is, + In this example, the schema mapping lines up with the fields in the JSON objects. That is, it contains these fields: id, name, and email. """); waitForInputToContinue(scanner); - String mappingARN = null; try { - CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); - mappingARN = response.schemaArn(); + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); + jsonSchemaMappingArn = response.schemaArn(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } + try { + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); + csvSchemaMappingArn = response.schemaArn(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + + waitForInputToContinue(scanner); logger.info(DASHES); @@ -148,11 +159,14 @@ Amazon Web Services (AWS) that helps organizations extract, link, and data profiling, and machine learning algorithms, it evaluates attributes like names or emails to detect duplicates or relationships, even with variations or inconsistencies. - The workflow outputs consolidated, de-duplicated data,\s + The workflow outputs consolidated, de-duplicated data. + + We will use the machine learning-based matching technique. """); waitForInputToContinue(scanner); try { - String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn + , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); @@ -175,7 +189,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("4. Get details for job "+jobId); + logger.info("4. While the matching job is running, let's look at other API methods. First, let's get details for job "+jobId); waitForInputToContinue(scanner); try { actions.getMatchingJobAsync(jobId, workflowName).join(); @@ -186,10 +200,10 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("5. Get Schema Mapping."); + logger.info("5. Get the schema mapping for the JSON data."); waitForInputToContinue(scanner); try { - actions.getSchemaMappingAsync(schemaName).join(); + actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); logger.info("Schema mapping retrieval completed."); } catch (CompletionException ce) { logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); @@ -204,7 +218,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("7. Tag the "+schemaName +"resource."); + logger.info("7. Tag the {} resource.", jsonSchemaMappingName); logger.info(""" Tags can help you organize and categorize your Entity Resolution resources. You can also use them to scope user permissions by granting a user permission @@ -212,7 +226,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, the SchemaMapping is tagged. """); - actions.tagEntityResource(mappingARN).join(); + actions.tagEntityResource(jsonSchemaMappingArn).join(); waitForInputToContinue(scanner); logger.info(DASHES); @@ -220,7 +234,11 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("8. Delete the AWS Entity Resolution Workflow."); logger.info(""" You cannot delete a workflow that is in a running state. - Would you like to wait for the workflow to complete. + Would you like to wait for the workflow that we started in step 3 to complete. + + If you choose not to wait, you will need to delete the workflow manually + in the AWS Management Console. + This can take up to 30 mins (y/n). """); String delAns = scanner.nextLine().trim(); @@ -228,6 +246,32 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("You selected to delete Entity Resolution Workflow."); waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); + logger.info("Number of input records: {}", metrics.inputRecords()); + logger.info("Number of match ids: {}", metrics.matchIDs()); + logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); + logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); + logger.info(""" + + The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + + Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; + For example 'Bob Smith Jr.' compared to 'Bob Smith'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, + the confidence level is lower for the differing email addresses. + + """); try { actions.deleteMatchingWorkflowAsync(workflowName).join(); logger.info("Workflow deleted successfully!"); @@ -239,6 +283,20 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); logger.info(DASHES); + logger.info(DASHES); + logger.info(""" + Now we delete the CloudFormation stack, which deletes + the resources that were created at the beginning + """); + waitForInputToContinue(scanner); + logger.info(DASHES); + try { + deleteResources(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); + } + logger.info(DASHES); logger.info("This concludes the AWS Entity Resolution scenario."); logger.info(DASHES); @@ -290,5 +348,11 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } } + private static void deleteResources(){ + CloudFormationHelper.emptyS3Bucket(glueBucketName); + CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); + logger.info("Resources deleted successfully!"); + + } } // snippet-end:[entityres.java2_scenario.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/TODO.md b/javav2/example_code/entityresolution/src/main/resources/TODO.md new file mode 100644 index 00000000000..8e3963dca2a --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/TODO.md @@ -0,0 +1,8 @@ +# Suggestions to improve the scenario + +Need to delete the schema mapping when you delete the workflow. + +Use two input data sources, since that is what a customer would do at a minimum. The input data for the scenario should contain records that do +and don't match. Make the second data source in CSV. + +When the job completes, display the results from the S3 bucket--both success and error. \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/data.csv b/javav2/example_code/entityresolution/src/main/resources/data.csv new file mode 100644 index 00000000000..3ec062e335d --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/data.csv @@ -0,0 +1,5 @@ + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/data.json b/javav2/example_code/entityresolution/src/main/resources/data.json new file mode 100644 index 00000000000..0375ab4e2be --- /dev/null +++ b/javav2/example_code/entityresolution/src/main/resources/data.json @@ -0,0 +1,3 @@ +{"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} +{"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} +{"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/glue.yaml b/javav2/example_code/entityresolution/src/main/resources/template.yaml similarity index 72% rename from javav2/example_code/entityresolution/src/main/resources/glue.yaml rename to javav2/example_code/entityresolution/src/main/resources/template.yaml index d09227c86fe..f0395929fa7 100644 --- a/javav2/example_code/entityresolution/src/main/resources/glue.yaml +++ b/javav2/example_code/entityresolution/src/main/resources/template.yaml @@ -1,14 +1,12 @@ Resources: - GlueDataBucket278CFAC6: + ErBucket6EA35F9D: Type: AWS::S3::Bucket Properties: - BucketName: glue-2cf5649393c7465f926ae00d0592eba8 - VersioningConfiguration: - Status: Enabled - UpdateReplacePolicy: Retain - DeletionPolicy: Retain + BucketName: erbucketf684533d2680435fa99d24b1bdaf5179 + UpdateReplacePolicy: Delete + DeletionPolicy: Delete Metadata: - aws:cdk:path: EntityResolutionCdkStack/GlueDataBucket/Resource + aws:cdk:path: EntityResolutionCdkStack/ErBucket/Resource GlueDatabase: Type: AWS::Glue::Database Properties: @@ -18,7 +16,7 @@ Resources: Name: entity_resolution_db Metadata: aws:cdk:path: EntityResolutionCdkStack/GlueDatabase - GlueTable: + jsongluetable: Type: AWS::Glue::Table Properties: CatalogId: @@ -26,7 +24,7 @@ Resources: DatabaseName: Ref: GlueDatabase TableInput: - Name: entity_resolution + Name: jsongluetable StorageDescriptor: Columns: - Name: id @@ -40,8 +38,8 @@ Resources: Fn::Join: - "" - - s3:// - - Ref: GlueDataBucket278CFAC6 - - /data/ + - Ref: ErBucket6EA35F9D + - /jsonData/ OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat SerdeInfo: Parameters: @@ -51,7 +49,43 @@ Resources: DependsOn: - GlueDatabase Metadata: - aws:cdk:path: EntityResolutionCdkStack/GlueTable + aws:cdk:path: EntityResolutionCdkStack/jsongluetable + csvgluetable: + Type: AWS::Glue::Table + Properties: + CatalogId: + Ref: AWS::AccountId + DatabaseName: + Ref: GlueDatabase + TableInput: + Name: csvgluetable + StorageDescriptor: + Columns: + - Name: id + Type: string + - Name: name + Type: string + - Name: email + Type: string + - Name: phone + Type: string + InputFormat: org.apache.hadoop.mapred.TextInputFormat + Location: + Fn::Join: + - "" + - - s3:// + - Ref: ErBucket6EA35F9D + - /csvData/ + OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat + SerdeInfo: + Parameters: + serialization.format: "1" + SerializationLibrary: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe + TableType: EXTERNAL_TABLE + DependsOn: + - GlueDatabase + Metadata: + aws:cdk:path: EntityResolutionCdkStack/csvgluetable EntityResolutionRoleB51A51D3: Type: AWS::IAM::Role Properties: @@ -101,32 +135,34 @@ Resources: - Ref: EntityResolutionRoleB51A51D3 Metadata: aws:cdk:path: EntityResolutionCdkStack/EntityResolutionRole/DefaultPolicy/Resource - OutputBucket7114EB27: - Type: AWS::S3::Bucket - Properties: - BucketName: entity-resolution-output-entityresolutioncdkstack - VersioningConfiguration: - Status: Enabled - UpdateReplacePolicy: Retain - DeletionPolicy: Retain - Metadata: - aws:cdk:path: EntityResolutionCdkStack/OutputBucket/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: - Analytics: v2:deflate64:H4sIAAAAAAAA/02MMQ+CMBSEfwt7eYLEuFsnFwy6m0cp5klpDW0lpul/N7SL0919d7k91M0BqgJXW4phKhX1EG4OxcRwtY9gGwgnLybpGB91dpE9lZcQ+KjP6LBHK7fyjr2SkRHOEDqjEkt6NYrEd4vZxcg6aY1fRNq03r19uv+n3OiBHBkd2QU/uKuPUEHdFC9LVC5eO5oldFl/L3LHkcUAAAA= + Analytics: v2:deflate64:H4sIAAAAAAAA/02MzQ7CIBCEn6V3WPuTvoD15EVTvZstRbOWgimgMYR3t4WLp5n5ZjI1VE0LZYEfy8U4cUUDhItDMbEV3YJtIOy9mKRj3V1nF9lDeQlhBQd0OKCVW3nFQcnICGcIvVGJJT0bReK7xexiZL20xi8ibU7evXy6/6ed0SM5MjqyI75xV1dQQls8LRFfvHY0S+iz/gCPIXoRxAAAAA== Metadata: aws:cdk:path: EntityResolutionCdkStack/CDKMetadata/Default Condition: CDKMetadataAvailable Outputs: - EntityResolutionArn: - Description: The ARN of the Glue Role + EntityResolutionRoleArn: + Description: The ARN of the EntityResolution Role Value: Fn::GetAtt: - EntityResolutionRoleB51A51D3 - Arn - GlueTableArn: - Description: The ARN of the Glue Table + JsonErGlueTableArn: + Description: The ARN of the Json Glue Table + Value: + Fn::Join: + - "" + - - "arn:aws:glue:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :table/ + - Ref: GlueDatabase + - /jsongluetable + CsvErGlueTableArn: + Description: The ARN of the CSV Glue Table Value: Fn::Join: - "" @@ -136,11 +172,11 @@ Outputs: - Ref: AWS::AccountId - :table/ - Ref: GlueDatabase - - /entity_resolution + - /csvgluetable GlueDataBucketName: Description: The name of the Glue Data Bucket Value: - Ref: GlueDataBucket278CFAC6 + Ref: ErBucket6EA35F9D Conditions: CDKMetadataAvailable: Fn::Or: @@ -224,17 +260,4 @@ Parameters: Type: AWS::SSM::Parameter::Value Default: /cdk-bootstrap/hnb659fds/version Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] -Rules: - CheckBootstrapVersion: - Assertions: - - Assert: - Fn::Not: - - Fn::Contains: - - - "1" - - "2" - - "3" - - "4" - - "5" - - Ref: BootstrapVersion - AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI. diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index 5e915b8f0ad..b1bb546d8b9 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; @@ -77,7 +76,7 @@ public static void setUp() { ] """; if (!actions.doesObjectExist(dataS3bucket)) { - actions.uploadLocalFileAsync(dataS3bucket, json); + actions.uploadInputData(dataS3bucket, json); } else { System.out.println("The JSON exists in " + dataS3bucket); } diff --git a/resources/cdk/entityresolution_resources/README.md b/resources/cdk/entityresolution_resources/README.md index 262d244b901..a686e715048 100644 --- a/resources/cdk/entityresolution_resources/README.md +++ b/resources/cdk/entityresolution_resources/README.md @@ -1,8 +1,9 @@ -# AWS Entity Resolution resources +# AWS Entity Resolution scenario resources ## Overview -Creates the following AWS resources for the AWS Entity Resolution scenario: +This AWS CDK Java application generates a AWS CloudFormation template. +The CloudFormation template creates the following resources for the AWS Entity Resolution scenario application: * An AWS IAM role that has permissions required to run this Scenario. * An AWS Glue table that provides the input data for the entity resolution matching workflow. @@ -11,50 +12,35 @@ Creates the following AWS resources for the AWS Entity Resolution scenario: ## ⚠️ Important -* Running this code might result in charges to your AWS account. +* When the template is used by the AWS Entity Resolution scenario application, + the resources it creates might result in charges to your account. * This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). -## Deploy resources - -You can use the AWS Cloud Development Kit (AWS CDK) or the AWS Command Line Interface -(AWS CLI) to deploy and destroy the resources for this example. - -### Deploy with the AWS CDK - -To deploy with the AWS CDK, you must install [Java JDK 17](https://www.oracle.com/ca-en/java/technologies/downloads/) and the -[AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). - -This example was built and tested with AWS CDK 2.135.0. - -Deploy AWS resources by running the following at a command prompt in this README's folder: +## Create a CloudFormation template +To output a template that creates the CloudFormation stack, execute the following CDK CLI command from the +`resources/cdk/entityresolution_resources` working directory: ``` -cdk deploy +cdk synth --yaml > ../../../javav2/example_code/entityresolution/src/main/resources/template.yaml ``` +The result of running this command puts the `template.yaml` file into the directory where +the scenario application can use it. -The stack takes a few minutes to deploy. When it completes, it prints output like -the following: +## Outputs generated +When the template is used and the stack is created by the AWS Entity Resolution scenario application, +the following outputs are generated and used in the application: ``` -Outputs: EntityResolutionCdkStack.EntityResolutionArn = arn:aws:iam::XXXXX:role/EntityResolutionCdkStack-EntityResolutionRoleB51A51-TSzkkBfrkbfm EntityResolutionCdkStack.GlueDataBucketName = glue-XXXXX3196d EntityResolutionCdkStack.GlueTableArn = arn:aws:glue:us-east-1:XXXXX:table/entity_resolution_db/entity_resolution ``` -Note - Copy these AWS resources into your AWS Entity Resolution scenario. These values are required for the program to successfully run. +## How stack-created resources are destroyed +AWS Entity Resolution scenario application destroys the resources created by the stack before it completes. -## Destroy resources - -### Destroy with the AWS CDK - -You can use the AWS CDK to destroy the resources by running the following: - -``` -cdk destroy -``` -## Additional resources +## Additional information * [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) * [AWS CLI User Guide for Version 2](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) diff --git a/resources/cdk/entityresolution_resources/cdk.json b/resources/cdk/entityresolution_resources/cdk.json new file mode 100644 index 00000000000..723b2f18b0c --- /dev/null +++ b/resources/cdk/entityresolution_resources/cdk.json @@ -0,0 +1,68 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + } +} diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java index 4aecc53409a..d46eeddf636 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -3,10 +3,18 @@ package com.myorg; -import software.amazon.awscdk.*; -import software.amazon.awscdk.services.iam.*; -import software.amazon.awscdk.services.s3.*; -import software.amazon.awscdk.services.glue.*; +import software.amazon.awscdk.CfnOutput; +import software.amazon.awscdk.CfnOutputProps; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.glue.CfnDatabase; +import software.amazon.awscdk.services.glue.CfnTable; +import software.amazon.awscdk.services.iam.ManagedPolicy; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.iam.ServicePrincipal; +import software.amazon.awscdk.services.s3.Bucket; import software.constructs.Construct; import java.util.List; @@ -20,13 +28,17 @@ public EntityResolutionCdkStack(final Construct scope, final String id) { public EntityResolutionCdkStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); + final String jsonGlueTableName = "jsongluetable"; + final String csvGlueTableName = "csvgluetable"; // 1. Create an S3 bucket for the Glue Data Table String uniqueId = UUID.randomUUID().toString().replace("-", ""); // Remove dashes to ensure compatibility - Bucket glueDataBucket = Bucket.Builder.create(this, "GlueDataBucket") - .bucketName("glue-" + uniqueId) - .versioned(true) - .build(); + + Bucket erBucket = Bucket.Builder.create(this, "ErBucket") + .bucketName("erbucket" + uniqueId) + .versioned(false) + .removalPolicy(RemovalPolicy.DESTROY) + .build(); // 2. Create a Glue database CfnDatabase glueDatabase = CfnDatabase.Builder.create(this, "GlueDatabase") @@ -37,7 +49,7 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build(); // 3. Create a Glue table referencing the S3 bucket - CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") +/* CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") .catalogId(this.getAccount()) .databaseName(glueDatabase.getRef()) // Ensure Glue Table references the database correctly .tableInput(CfnTable.TableInputProperty.builder() @@ -58,10 +70,26 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build()) .build()) .build()) - .build(); + .build();*/ + final CfnTable jsonErGlueTable = createGlueTable(jsonGlueTableName + , jsonGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string") + , "s3://" + erBucket.getBucketName() + "/jsonData/" + , "org.openx.data.jsonserde.JsonSerDe"); + + // Ensure Glue Table is created after the Database + jsonErGlueTable.addDependency(glueDatabase); + + final CfnTable csvErGlueTable = createGlueTable(csvGlueTableName + , csvGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string", "phone", "string") + , "s3://" + erBucket.getBucketName() + "/csvData/" + , "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe"); // Ensure Glue Table is created after the Database - glueTable.addDependency(glueDatabase); + csvErGlueTable.addDependency(glueDatabase); // 4. Create an IAM Role for AWS Entity Resolution Role entityResolutionRole = Role.Builder.create(this, "EntityResolutionRole") @@ -74,6 +102,11 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St )) .build(); + new CfnOutput(this, "EntityResolutionRoleArn", CfnOutputProps.builder() + .value(entityResolutionRole.getRoleArn()) + .description("The ARN of the EntityResolution Role") + .build()); + // Add custom permissions for Entity Resolution entityResolutionRole.addToPolicy(PolicyStatement.Builder.create() .actions(List.of( @@ -83,35 +116,58 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .resources(List.of("*")) // Adjust permissions if needed .build()); - // 5. Create an S3 bucket for output data - Bucket outputBucket = Bucket.Builder.create(this, "OutputBucket") - .bucketName("entity-resolution-output-" + id.toLowerCase()) - .versioned(true) - .build(); - - // 6. Output the Role ARN - new CfnOutput(this, "EntityResolutionArn", CfnOutputProps.builder() - .value(entityResolutionRole.getRoleArn()) - .description("The ARN of the Glue Role") + // ------------------------ OUTPUTS -------------------------------------- + new CfnOutput(this, "JsonErGlueTableArn", CfnOutputProps.builder() + .value(createGlueTableArn(jsonErGlueTable, jsonGlueTableName)) + .description("The ARN of the Json Glue Table") .build()); - // 7. Construct and output the Glue Table ARN - String glueTableArn = String.format("arn:aws:glue:%s:%s:table/%s/%s", - this.getRegion(), // Region where the stack is deployed - this.getAccount(), // AWS account ID - glueDatabase.getRef(), // Glue database name (resolved reference) - "entity_resolution" // Corrected table name - ); - - new CfnOutput(this, "GlueTableArn", CfnOutputProps.builder() - .value(glueTableArn) - .description("The ARN of the Glue Table") - .build()); + new CfnOutput(this, "CsvErGlueTableArn", CfnOutputProps.builder() + .value(createGlueTableArn(csvErGlueTable, csvGlueTableName)) + .description("The ARN of the CSV Glue Table") + .build()); - // 8. Output the name of the Glue Data Bucket new CfnOutput(this, "GlueDataBucketName", CfnOutputProps.builder() - .value(glueDataBucket.getBucketName()) // Outputs the bucket name + .value(erBucket.getBucketName()) // Outputs the bucket name .description("The name of the Glue Data Bucket") .build()); } + + CfnTable createGlueTable(String id, String tableName, String databaseRef, Map schemaMap, String dataLocation, String serializationLib){ + return CfnTable.Builder.create(this, id) + .catalogId(this.getAccount()) + .databaseName(databaseRef) // Ensure Glue Table references the database correctly + .tableInput(CfnTable.TableInputProperty.builder() + .name(tableName) // Fixed table name reference + .tableType("EXTERNAL_TABLE") + .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() + .columns(createColumns(schemaMap)) + .location(dataLocation) // Append subpath for data + .inputFormat("org.apache.hadoop.mapred.TextInputFormat") + .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") + .serdeInfo(CfnTable.SerdeInfoProperty.builder() + .serializationLibrary(serializationLib) // Set JSON SerDe + .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON + .build()) + .build()) + .build()) + .build(); + } + List createColumns(Map schemaMap) { + return schemaMap.entrySet().stream() + .map(entry -> CfnTable.ColumnProperty.builder() + .name(entry.getKey()) + .type(entry.getValue()) + .build()) + .toList(); + } + + String createGlueTableArn(CfnTable glueTable, String glueTableName) { + return String.format("arn:aws:glue:%s:%s:table/%s/%s" + , this.getRegion() + , this.getAccount() + , glueTable.getDatabaseName() + , glueTableName + ); + } } diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 6099ece90f5..6e79288f02e 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -3,24 +3,26 @@ This AWS Entity Resolution basic scenario demonstrates how to interact with the ## Key Operations -1. **Create an AWS Entity Resolution Schema Mapping**: - - This step creates an AWS Entity Resolution Schema Mapping by invoking the `createSchemaMapping` method. +1. **Create an AWS Entity Resolution schema mapping**: + - This step creates an AWS Entity Resolution schema mapping by invoking the `createSchemaMapping` method. -2. **Create an AWS Entity Resolution Workflow**: - - This step creates an AWS Entity Resolution matching Workflow by invoking the `createMatchingWorkflow` method. +2. **Create an AWS Entity Resolution workflow**: + - This step creates an AWS Entity Resolution matching workflow by invoking the `createMatchingWorkflow` method. -3. **Start Matching Workflow**: - - This step starts the AWS Entity Resolution matching Workflow by invoking the `startMatchingJob` method. +3. **Start a matching aorkflow**: + - This step starts the AWS Entity Resolution matching workflow by invoking the `startMatchingJob` method. -4. **Get Workflow Job Details**: - - This step gets workflow job details by `getMatchingJob` method. +4. **Get workflow job details**: + - This step gets workflow job details by invoking the `getMatchingJob` method. -**Note** See the Eng spec for a full listing of operations. +**Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. See the Readme file at resources/cdk/entityresolution_resources. +This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, +an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. +See the resources [Readme](../../../resources/cdk/entityresolution_resources/README.md) file. ## Implementations diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 990a899296a..f7c4c616544 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -1,62 +1,88 @@ -# AWS Entity Resolution Service Scenario Specification +# Specification for the AWS Entity Resolution Service Scenario ## Overview -This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution using an AWS SDK. It demonstrates various tasks such as creating a Schema Mapping, creating an matching workflow, starting the workflow, and so on. Finally this scenario demonstrates how to clean up resources. Its purpose is to demonstrate how to get up and running with AWS Entity Resolution and an AWS SDK. + +This SDK Basics scenario demonstrates how to interact with AWS Entity Resolution +using an AWS SDK. It demonstrates various tasks such as creating a schema +mapping, creating an matching workflow, starting a workflow, and so on. Finally, +this scenario demonstrates how to clean up resources. ## Resources -This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, an AWS Glue database and a table, and two S3 buckets. A CDK script is provided to create these resources. + +This Basics scenario requires an IAM role that has permissions to work with the +AWS Entity Resolution service, an AWS Glue database and a table, and two S3 +buckets. +A [CDK script](../../../resources/cdk/entityresolution_resources/README.md +) is provided to create these resources. ## Hello AWS Entity Resolution -This program is intended for users not familiar with the AWS Entity Resolution Service to easily get up and running. The program uses a `listMatchingWorkflowsPaginator` to demonstrate how you can read through workflow information. + +This program is intended for users not familiar with the AWS Entity Resolution +Service to easily get up and running. The program uses a +`listMatchingWorkflowsPaginator` to demonstrate how you can read through +workflow information. ## Basics Scenario Program Flow + The AWS Entity Resolution Basics scenario executes the following operations. -1. **Create a Schema Mapping**: - - Description: Creates a schema mapping invoking the `createSchemaMapping` method. - - Exception Handling: Check to see if a `ConflictException` is thrown. - If it is thrown, display the information and end the program. +1. **Create a schema mapping**: + - Description: Creates a schema mapping by invoking the + `createSchemaMapping` method. + - Exception Handling: Check to see if a `ConflictException` is thrown, which + indicates that the schema mapping already exists. If the exception is + thrown, display the information and end the program. 2. **Create a Matching Workflow**: - - Description: Creates a new matching workflow, defining how entities should be resolved and matched.. - - The method `createMatchingWorkflow` is called. - - Exception Handling: Check to see if a `ConflictException` is thrown if a conflict in the current state of the resource exists. If so, - display the message and end the program. + - Description: Creates a new matching workflow that defines how entities + should be resolved and matched. The method `createMatchingWorkflow` is + called. + - Exception Handling: Check to see if a `ConflictException` is thrown, which + is thrown if the matching workflow already exists. If so, display the + message and end the program. 3. **Start Matching Workflow**: - - Description: Initiates a matching workflow to process entity resolution based on predefined configurations. - - The method `startMatchingJob` is called to start the matching workflow. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + - Description: Initiates a matching workflow by calling the + `startMatchingJob` method to process entity resolution based on predefined + configurations. + - Exception Handling: Check to see if an `ConflictException` is thrown, + which indicates that the matching workflow job is already running. If the + exception is thrown, display the message and end the program. 4. **Get Workflow Job Details**: - - Description: Retrieves details about a specific matching workflow job. - - This step uses the method `getMatchingJob`. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. - + - Description: Retrieves details about a specific matching workflow job by + calling the `getMatchingJob` method. + - Exception Handling: Check to see if an `ResourceNotFoundException` is + thrown, which indicates that the workflow cannot be found. If the + exception is thrown, display the message and end the program. 5. **List Matching Workflows**: - - Description: Lists all matching workflows created within the account. - - This step uses the method `listMatchingWorkflows`. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program. + - Description: Lists all matching workflows created within the account by + calling the `listMatchingWorkflows` method. + - Exception Handling: Check to see if an `CompletionException` is thrown. If + so, display the message and end the program. 6. **Get Schema Mapping**: - - Description: Lists all schema mappings available in the account. - - The method `createPortal` is called. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program + - Description: Returns the `SchemaMapping` of a given name by calling the + `getSchemaMapping` method. + - Exception Handling: Check to see if a `ResourceNotFoundException` is + thrown. If so, display the message and end the program. 7. **Tag Resource**: - - Description: Adds tags associated with an AWS Entity Resolution resource. - - The method `tagResource` is called. - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program - -8. **Delete Matching Workflow**: - - Description: Deletes a specified matching workflowy. - - The methods `deleteMatchingWorkflow` is called. - - - Exception Handling: Check to see if an `CompletionException` is thrown. If so, display the message and end the program - + - Description: Adds tags associated with an AWS Entity Resolution resource + by calling the`tagResource` method. + - Exception Handling: Check to see if a `ResourceNotFoundException` is + thrown. If so, display the message and end the program +8. **Delete Matching Workflow**: + - Description: Deletes a specified matching workflow by calling the + `deleteMatchingWorkflow` method. + - Exception Handling: Check to see if an `ConflictException` is thrown. If + so, display the message and end the program. ### Program execution -The following shows the output of the AWS Entity Resolution Basics scenario in the console. + +The following shows the output of the AWS Entity Resolution Basics scenario in +the console. ``` Welcome to the AWS Entity Resolution Scenario. @@ -243,19 +269,20 @@ This concludes the AWS Entity Resolution scenario. ## SOS Tags The following table describes the metadata used in this Basics Scenario. -| action | metadata file | metadata key | -|-------------------------|------------------------|---------------------------------------- | -| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | -| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | -| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | -| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | -| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | -| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | -| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | -| `tagResource ` | entity_metadata.yaml | entity_TagResource | -| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | -| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | -| `scenario` | entity_metadata.yaml | entity_Scenario | + +| action | metadata file | metadata key | +|-------------------------|------------------------|-------------------------------| +| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | +| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | +| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | +| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | +| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | +| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | +| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | +| `tagResource ` | entity_metadata.yaml | entity_TagResource | +| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | +| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | +| `scenario` | entity_metadata.yaml | entity_Scenario | From 87ee468305b06042d0b645a5040df51cc9779523 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 24 Feb 2025 16:20:57 -0500 Subject: [PATCH 123/144] updated validation file --- .doc_gen/validation.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.doc_gen/validation.yaml b/.doc_gen/validation.yaml index ea3da0450aa..fe87874aa32 100644 --- a/.doc_gen/validation.yaml +++ b/.doc_gen/validation.yaml @@ -1,6 +1,7 @@ allow_list: # Git commits - "cd5e746ec203c8c3c61647e0886a8df8c1e78e41" + - "erbucketf684533d2680435fa99d24b1bdaf5179" - "725feb26d6f73bc1d83dbbe075ae8ea991efb245" - "e9772d140489982e0e3704fea5ee93d536f1e275" # Safe look-alikes, mostly tokens and paths that happen to be 40 characters. From b63fd0da2ae77329304ee26bd483de50184798f9 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Mon, 24 Feb 2025 16:24:17 -0500 Subject: [PATCH 124/144] updated validation file --- .../com/myorg/EntityResolutionCdkTest.java | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java diff --git a/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java b/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java deleted file mode 100644 index ac832c96bdb..00000000000 --- a/resources/cdk/entityresolution_resources/src/test/java/com/myorg/EntityResolutionCdkTest.java +++ /dev/null @@ -1,26 +0,0 @@ -// package com.myorg; - -// import software.amazon.awscdk.App; -// import software.amazon.awscdk.assertions.Template; -// import java.io.IOException; - -// import java.util.HashMap; - -// import org.junit.jupiter.api.Test; - -// example test. To run these tests, uncomment this file, along with the -// example resource in java/src/main/java/com/myorg/EntityResolutionCdkStack.java -// public class EntityResolutionCdkTest { - -// @Test -// public void testStack() throws IOException { -// App app = new App(); -// EntityResolutionCdkStack stack = new EntityResolutionCdkStack(app, "test"); - -// Template template = Template.fromStack(stack); - -// template.hasResourceProperties("AWS::SQS::Queue", new HashMap() {{ -// put("VisibilityTimeout", 300); -// }}); -// } -// } From 9b548296411cb60084a408999bfbbda1431c9aa2 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 10:42:05 -0500 Subject: [PATCH 125/144] updated logic --- .../entity/scenario/EntityResScenario.java | 72 ++++++++++--------- .../src/test/java/EntityResTests.java | 18 +++-- scenarios/basics/entity_resolution/README.md | 38 ++++++---- 3 files changed, 77 insertions(+), 51 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index a0551a628a9..1b17fd03f08 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -28,7 +28,6 @@ public class EntityResScenario { private static String workflowName = "workflow-"+ UUID.randomUUID(); public static void main(String[] args) throws InterruptedException { - String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); String jsonSchemaMappingArn = null; String csvSchemaMappingName = "csv-" + UUID.randomUUID(); @@ -126,13 +125,14 @@ Amazon Web Services (AWS) that helps organizations extract, link, and and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - In this example, the schema mapping lines up with the fields in the JSON objects. That is, + In this example, the schema mapping lines up with the fields in the JSON ans CSV objects. That is, it contains these fields: id, name, and email. """); waitForInputToContinue(scanner); try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); jsonSchemaMappingArn = response.schemaArn(); + logger.info("The JSON schema mapping ARN is "+jsonSchemaMappingArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -141,12 +141,11 @@ Amazon Web Services (AWS) that helps organizations extract, link, and try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); csvSchemaMappingArn = response.schemaArn(); + logger.info("The CSV schema mapping ARN is "+csvSchemaMappingArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } - - waitForInputToContinue(scanner); logger.info(DASHES); @@ -231,19 +230,19 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("8. Delete the AWS Entity Resolution Workflow."); + logger.info("8. View the results of the AWS Entity Resolution Workflow."); logger.info(""" - You cannot delete a workflow that is in a running state. - Would you like to wait for the workflow that we started in step 3 to complete. + You cannot view the result of the workflow that is in a running state. + In order to view the results, you need to wait for the workflow that we started in step 3 to complete. - If you choose not to wait, you will need to delete the workflow manually - in the AWS Management Console. + If you choose not to wait, you cannot view the results or delete the workflow. You would have to + perform both tasks manually in the AWS Management Console. This can take up to 30 mins (y/n). """); - String delAns = scanner.nextLine().trim(); - if (delAns.equalsIgnoreCase("y")) { - logger.info("You selected to delete Entity Resolution Workflow."); + String viewAns = scanner.nextLine().trim(); + if (viewAns.equalsIgnoreCase("y")) { + logger.info("You selected to view the Entity Resolution Workflow results."); waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); @@ -272,30 +271,38 @@ Amazon Web Services (AWS) that helps organizations extract, link, and the confidence level is lower for the differing email addresses. """); - try { - actions.deleteMatchingWorkflowAsync(workflowName).join(); - logger.info("Workflow deleted successfully!"); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); - } - } - waitForInputToContinue(scanner); - logger.info(DASHES); - logger.info(DASHES); - logger.info(""" + logger.info("Do you want to delete the resources, including workflow?"); + String delAns = scanner.nextLine().trim(); + if (delAns.equalsIgnoreCase("y")) { + try { + actions.deleteMatchingWorkflowAsync(workflowName).join(); + logger.info("Workflow deleted successfully!"); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + waitForInputToContinue(scanner); + logger.info(DASHES); + logger.info(""" Now we delete the CloudFormation stack, which deletes the resources that were created at the beginning """); + waitForInputToContinue(scanner); + logger.info(DASHES); + try { + deleteResources(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); + } + + } else { + logger.info("You can delete the Workflow later in the AWS Management console."); + } + } waitForInputToContinue(scanner); logger.info(DASHES); - try { - deleteResources(); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); - } logger.info(DASHES); logger.info("This concludes the AWS Entity Resolution scenario."); @@ -336,13 +343,13 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota // Check workflow status every 60 seconds if (secondsElapsed % 60 == 0 || remainingTime <= 0) { if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { - logger.info(""); // Move to the next line after countdown + logger.info(""); // Move to the next line after countdown. logger.info("Countdown complete: Workflow is in SUCCEEDED state!"); break; } } - // If countdown reaches zero, reset it for continuous countdown + // If countdown reaches zero, reset it for continuous countdown. if (remainingTime <= 0) { secondsElapsed = 0; } @@ -352,7 +359,6 @@ private static void deleteResources(){ CloudFormationHelper.emptyS3Bucket(glueBucketName); CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); logger.info("Resources deleted successfully!"); - } } // snippet-end:[entityres.java2_scenario.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index b1bb546d8b9..f3610d22dc9 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -75,11 +75,16 @@ public static void setUp() { } ] """; - if (!actions.doesObjectExist(dataS3bucket)) { - actions.uploadInputData(dataS3bucket, json); - } else { - System.out.println("The JSON exists in " + dataS3bucket); - } + + String csv = """ + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + """; + + actions.uploadInputData(dataS3bucket, json, csv); } @Test @@ -99,7 +104,8 @@ public void testCreateMapping() { @Order(2) public void testCreateMappingWorkflow() { assertDoesNotThrow(() -> { - workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, outputBucket, inputGlueTableArn, schemaName).join(); + //workflowArn = actions.actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn + // , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); assertNotNull(workflowArn); }); logger.info("Test 2 passed"); diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 6e79288f02e..05a255335c4 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -1,27 +1,41 @@ +# AWS Entity Resolution Java Program + ## Overview -This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. The scenario covers various operations such as creating a schema mapping, creating a matching workflow, starting a matching job, and so on. +This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. This Java application demonstrates how to use AWS Entity Resolution to integrate and deduplicate data from multiple sources using machine learning-based matching. The program walks through setting up AWS resources, uploading structured data, defining schema mappings, creating a matching workflow, and running a matching job. -## Key Operations -1. **Create an AWS Entity Resolution schema mapping**: - - This step creates an AWS Entity Resolution schema mapping by invoking the `createSchemaMapping` method. +**Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. -2. **Create an AWS Entity Resolution workflow**: - - This step creates an AWS Entity Resolution matching workflow by invoking the `createMatchingWorkflow` method. +## Features -3. **Start a matching aorkflow**: - - This step starts the AWS Entity Resolution matching workflow by invoking the `startMatchingJob` method. +1. Uses AWS CloudFormation to create necessary resources: -4. **Get workflow job details**: - - This step gets workflow job details by invoking the `getMatchingJob` method. +- AWS Glue Data Catalog table +- AWS IAM role -**Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. +- AWS S3 bucket + +- AWS Entity Resolution Schema + +2. Uploads sample JSON and CSV data to S3 + +3. Creates schema mappings for JSON and CSV datasets + +4. Creates and starts an Entity Resolution matching workflow + +5. Retrieves job details and schema mappings + +6. Lists available schema mappings + +7. Tags AWS resources for better organization + +8. Views the results of the workflow ## Resources This Basics scenario requires an IAM role that has permissions to work with the AWS Entity Resolution service, -an AWS Glue database, and two S3 buckets. A CDK script is provided to create these resources. +an AWS Glue database, and an S3 bucket. A CDK script is provided to create these resources. See the resources [Readme](../../../resources/cdk/entityresolution_resources/README.md) file. ## Implementations From 217aae60cb2562f67a2a61c569cd756c6262faab Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 10:43:52 -0500 Subject: [PATCH 126/144] updated readme --- scenarios/basics/entity_resolution/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scenarios/basics/entity_resolution/README.md b/scenarios/basics/entity_resolution/README.md index 05a255335c4..14679cbcc52 100644 --- a/scenarios/basics/entity_resolution/README.md +++ b/scenarios/basics/entity_resolution/README.md @@ -1,7 +1,7 @@ -# AWS Entity Resolution Java Program +# AWS Entity Resolution Program ## Overview -This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. This Java application demonstrates how to use AWS Entity Resolution to integrate and deduplicate data from multiple sources using machine learning-based matching. The program walks through setting up AWS resources, uploading structured data, defining schema mappings, creating a matching workflow, and running a matching job. +This AWS Entity Resolution basic scenario demonstrates how to interact with the AWS Entity Resolution service using an AWS SDK. This application demonstrates how to use AWS Entity Resolution to integrate and deduplicate data from multiple sources using machine learning-based matching. The program walks through setting up AWS resources, uploading structured data, defining schema mappings, creating a matching workflow, and running a matching job. **Note:** See the [specification document](SPECIFICATION.md) for a complete list of operations. From ea8584c461a73189ff0264dacb2acb462fe5f5c1 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 14:36:57 -0500 Subject: [PATCH 127/144] updated YAML file --- .../metadata/entityresolution_metadata.yaml | 12 ++ .../entity/scenario/EntityResActions.java | 27 +++- .../entity/scenario/EntityResScenario.java | 134 ++++++++++-------- .../src/test/java/EntityResTests.java | 128 +++++++---------- .../basics/entity_resolution/SPECIFICATION.md | 1 + 5 files changed, 156 insertions(+), 146 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index c9c23f58645..c25218b5164 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -14,6 +14,18 @@ entityresolution_Hello: - entityres.java2_hello.main services: entityresolution: {listMatchingWorkflows} +entityresolution_DeleteSchemaMapping: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/entityresolution + excerpts: + - description: + snippet_tags: + - entityres.java2_delete_mappings.main + services: + entityresolution: {entityresolution_DeleteSchemaMapping} entityresolution_TagEntityResource: languages: Java: diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 7876abfcf46..5c6e24c6016 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -17,8 +17,8 @@ import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.DeleteSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; -import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; @@ -41,7 +41,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { @@ -113,8 +112,28 @@ public static S3AsyncClient getS3AsyncClient() { return s3AsyncClient; } - // snippet-start:[entityres.java2_list_mappings.main] + // snippet-start:[entityres.java2_delete_mappings.main] + /** + * Deletes the schema mapping asynchronously. + * + * @param schemaName the name of the schema to delete + * @return a {@link CompletableFuture} that completes when the schema mapping is deleted successfully, + * or throws a {@link RuntimeException} if the deletion fails + */ + public CompletableFuture deleteSchemaMappingAsync(String schemaName) { + DeleteSchemaMappingRequest request = DeleteSchemaMappingRequest.builder() + .schemaName(schemaName) + .build(); + + return getResolutionAsyncClient().deleteSchemaMapping(request) + .thenRun(() -> logger.info("Schema mapping '{}' deleted successfully.", schemaName)) + .exceptionally(ex -> { + throw new RuntimeException("Failed to delete schema mapping: " + schemaName, ex); + }); + } + // snippet-end:[entityres.java2_delete_mappings.main] + // snippet-start:[entityres.java2_list_mappings.main] /** * Lists the schema mappings associated with the current AWS account. This method uses an asynchronous paginator to * retrieve the schema mappings, and prints the name of each schema mapping to the console. @@ -162,7 +181,6 @@ public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) // snippet-end:[entityres.java2_delete_matching_workflow.main] // snippet-start:[entityres.java2_create_schema.main] - /** * Creates a schema mapping asynchronously. * @@ -203,7 +221,6 @@ public CompletableFuture createSchemaMappingAsync(S // snippet-end:[entityres.java2_create_schema.main] // snippet-start:[entityres.java2_get_schema_mapping.main] - /** * Retrieves the schema mapping asynchronously. * diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 1b17fd03f08..9899c69a34e 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.JobMetrics; import java.util.Map; @@ -25,13 +26,12 @@ public class EntityResScenario { private static final String JSON_GLUE_TABLE_ARN_KEY = "JsonErGlueTableArn"; private static final String CSV_GLUE_TABLE_ARN_KEY = "CsvErGlueTableArn"; private static String glueBucketName; - private static String workflowName = "workflow-"+ UUID.randomUUID(); + private static String workflowName = "workflow-" + UUID.randomUUID(); public static void main(String[] args) throws InterruptedException { String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); String jsonSchemaMappingArn = null; String csvSchemaMappingName = "csv-" + UUID.randomUUID(); - String csvSchemaMappingArn = null; String roleARN; String csvGlueTableArn; String jsonGlueTableArn; @@ -69,15 +69,15 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(""" - To prepare the AWS resources needed for this scenario application, the next step uploads - a CloudFormation template whose resulting stack creates the following resources: - - An AWS Glue Data Catalog table - - An AWS IAM role - - An AWS S3 bucket - - An AWS Entity Resolution Schema - - It can take a couple minutes for the Stack to finish creating the resources. - """); + To prepare the AWS resources needed for this scenario application, the next step uploads + a CloudFormation template whose resulting stack creates the following resources: + - An AWS Glue Data Catalog table + - An AWS IAM role + - An AWS S3 bucket + - An AWS Entity Resolution Schema + + It can take a couple minutes for the Stack to finish creating the resources. + """); waitForInputToContinue(scanner); logger.info("Generating resources..."); CloudFormationHelper.deployCloudFormationStack(STACK_NAME); @@ -95,19 +95,19 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Entity Resolution service. */ String json = """ - {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} - {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} - {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} - """; + {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} + {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} + {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} + """; logger.info("Upload the following JSON objects to the {} S3 bucket.", glueBucketName); logger.info(json); String csv = """ - id,name,email,phone - 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 - 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 - 3,Charlie Black,charlie.black@company.com,345-567-1234 - 7,Jane E. Doe,jane_doe@company.com,111-222-3333 - """; + id,name,email,phone + 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 + 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 + 3,Charlie Black,charlie.black@company.com,345-567-1234 + 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + """; logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); logger.info(csv); waitForInputToContinue(scanner); @@ -131,8 +131,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and waitForInputToContinue(scanner); try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); - jsonSchemaMappingArn = response.schemaArn(); - logger.info("The JSON schema mapping ARN is "+jsonSchemaMappingArn); + jsonSchemaMappingName = response.schemaName(); + logger.info("The JSON schema mapping name is " + jsonSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -140,8 +140,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); - csvSchemaMappingArn = response.schemaArn(); - logger.info("The CSV schema mapping ARN is "+csvSchemaMappingArn); + csvSchemaMappingName = response.schemaName(); + logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); @@ -159,13 +159,12 @@ Amazon Web Services (AWS) that helps organizations extract, link, and it evaluates attributes like names or emails to detect duplicates or relationships, even with variations or inconsistencies. The workflow outputs consolidated, de-duplicated data. - + We will use the machine learning-based matching technique. """); waitForInputToContinue(scanner); try { - String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn - , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn, jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); @@ -188,7 +187,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(DASHES); logger.info(DASHES); - logger.info("4. While the matching job is running, let's look at other API methods. First, let's get details for job "+jobId); + logger.info("4. While the matching job is running, let's look at other API methods. First, let's get details for job " + jobId); waitForInputToContinue(scanner); try { actions.getMatchingJobAsync(jobId, workflowName).join(); @@ -202,8 +201,9 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("5. Get the schema mapping for the JSON data."); waitForInputToContinue(scanner); try { - actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); - logger.info("Schema mapping retrieval completed."); + GetSchemaMappingResponse response = actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); + jsonSchemaMappingArn = response.schemaArn(); + logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); } @@ -234,16 +234,15 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info(""" You cannot view the result of the workflow that is in a running state. In order to view the results, you need to wait for the workflow that we started in step 3 to complete. - + If you choose not to wait, you cannot view the results or delete the workflow. You would have to perform both tasks manually in the AWS Management Console. - + This can take up to 30 mins (y/n). """); String viewAns = scanner.nextLine().trim(); if (viewAns.equalsIgnoreCase("y")) { logger.info("You selected to view the Entity Resolution Workflow results."); - waitForInputToContinue(scanner); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); logger.info("Number of input records: {}", metrics.inputRecords()); @@ -251,28 +250,28 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); logger.info(""" - - The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: - - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - - Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; - For example 'Bob Smith Jr.' compared to 'Bob Smith'. - The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, - the confidence level is lower for the differing email addresses. - - """); + + The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + + Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; + For example 'Bob Smith Jr.' compared to 'Bob Smith'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, + the confidence level is lower for the differing email addresses. + + """); - logger.info("Do you want to delete the resources, including workflow?"); + logger.info("Do you want to delete the resources, including the workflow?"); String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { try { @@ -282,12 +281,22 @@ Amazon Web Services (AWS) that helps organizations extract, link, and Throwable cause = ce.getCause(); logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); } + + try { + // Delete both schema mappings. + actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); + actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); + logger.info("Both schema mappings were deleted successfully!"); + } catch (RuntimeException e) { + logger.error("Error deleting schema mapping: {}", e.getMessage()); + } + waitForInputToContinue(scanner); logger.info(DASHES); logger.info(""" - Now we delete the CloudFormation stack, which deletes - the resources that were created at the beginning - """); + Now we delete the CloudFormation stack, which deletes + the resources that were created at the beginning + """); waitForInputToContinue(scanner); logger.info(DASHES); try { @@ -330,17 +339,17 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota int secondsElapsed = 0; while (true) { - // Calculate display minutes and seconds + // Calculate display minutes and seconds. int remainingTime = totalSeconds - secondsElapsed; int displayMinutes = remainingTime / 60; int displaySeconds = remainingTime % 60; - // Print the countdown + // Print the countdown. System.out.printf("\r%02d:%02d", displayMinutes, displaySeconds); Thread.sleep(1000); // Wait for 1 second secondsElapsed++; - // Check workflow status every 60 seconds + // Check workflow status every 60 seconds. if (secondsElapsed % 60 == 0 || remainingTime <= 0) { if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { logger.info(""); // Move to the next line after countdown. @@ -355,7 +364,8 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } } - private static void deleteResources(){ + + private static void deleteResources() { CloudFormationHelper.emptyS3Bucket(glueBucketName); CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); logger.info("Resources deleted successfully!"); diff --git a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java index f3610d22dc9..03f2c75980d 100644 --- a/javav2/example_code/entityresolution/src/test/java/EntityResTests.java +++ b/javav2/example_code/entityresolution/src/test/java/EntityResTests.java @@ -2,23 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 +import com.example.entity.scenario.CloudFormationHelper; import com.example.entity.scenario.EntityResActions; -import com.google.gson.Gson; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; -import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; -import java.util.Random; + +import java.util.Map; +import java.util.UUID; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; import org.slf4j.Logger; @@ -28,33 +25,40 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class EntityResTests { private static final Logger logger = LoggerFactory.getLogger(EntityResTests.class); - private static String workflowName = ""; - private static String schemaName = ""; private static String roleARN = ""; - private static String dataS3bucket = ""; - private static String outputBucket = ""; - private static String inputGlueTableArn = ""; - private static String mappingARN = ""; + private static String csvMappingARN = ""; + private static String jsonMappingARN = ""; private static String jobId = ""; - private static String workflowArn =""; - private static EntityResActions actions = new EntityResActions(); + private static String glueBucketName = ""; + + private static String csvGlueTableArn = ""; + + private static String jsonGlueTableArn = ""; + + private static final String STACK_NAME = "EntityResolutionCdkStack"; + + private static final String ENTITY_RESOLUTION_ROLE_ARN_KEY = "EntityResolutionRoleArn"; + private static final String GLUE_DATA_BUCKET_NAME_KEY = "GlueDataBucketName"; + private static final String JSON_GLUE_TABLE_ARN_KEY = "JsonErGlueTableArn"; + private static final String CSV_GLUE_TABLE_ARN_KEY = "CsvErGlueTableArn"; + + private static String workflowArn = ""; + private static final String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); + private static final String csvSchemaMappingName = "csv-" + UUID.randomUUID(); + private static final String workflowName = "workflow-"+ UUID.randomUUID(); + private static final EntityResActions actions = new EntityResActions(); @BeforeAll public static void setUp() { - Random random = new Random(); - int randomValue = random.nextInt(10000) + 1; - workflowName = "MyMatchingWorkflow"+randomValue; - schemaName = "schema"+randomValue; - Gson gson = new Gson(); - String jsonVal = getSecretValues(); - SecretValues values = gson.fromJson(jsonVal, SecretValues.class); - roleARN = values.getRoleARN(); - dataS3bucket = values.getDataS3bucket(); - outputBucket = values.getOutputBucket(); - inputGlueTableArn = values.getInputGlueTableArn(); + CloudFormationHelper.deployCloudFormationStack(STACK_NAME); + Map outputsMap = CloudFormationHelper.getStackOutputsAsync(STACK_NAME).join(); + roleARN = outputsMap.get(ENTITY_RESOLUTION_ROLE_ARN_KEY); + glueBucketName = outputsMap.get(GLUE_DATA_BUCKET_NAME_KEY); + csvGlueTableArn = outputsMap.get(CSV_GLUE_TABLE_ARN_KEY); + jsonGlueTableArn = outputsMap.get(JSON_GLUE_TABLE_ARN_KEY); String json = """ [ @@ -84,7 +88,7 @@ public static void setUp() { 7,Jane E. Doe,jane_doe@company.com,111-222-3333 """; - actions.uploadInputData(dataS3bucket, json, csv); + actions.uploadInputData(glueBucketName, json, csv); } @Test @@ -92,9 +96,15 @@ public static void setUp() { @Order(1) public void testCreateMapping() { assertDoesNotThrow(() -> { - CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(schemaName).join(); - mappingARN = response.schemaArn(); - assertNotNull(mappingARN); + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); + jsonMappingARN = response.schemaArn(); + assertNotNull(jsonMappingARN); + }); + + assertDoesNotThrow(() -> { + CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(csvSchemaMappingName).join(); + csvMappingARN = response.schemaArn(); + assertNotNull(csvMappingARN); }); logger.info("Test 1 passed"); } @@ -104,8 +114,7 @@ public void testCreateMapping() { @Order(2) public void testCreateMappingWorkflow() { assertDoesNotThrow(() -> { - //workflowArn = actions.actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn - // , jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn, jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); assertNotNull(workflowArn); }); logger.info("Test 2 passed"); @@ -137,7 +146,7 @@ public void testGetJobDetails() { @Order(5) public void testtSchemaMappingDetails() { assertDoesNotThrow(() -> { - actions.getSchemaMappingAsync(schemaName).join(); + actions.getSchemaMappingAsync(jsonSchemaMappingName).join(); }); logger.info("Test 5 passed"); } @@ -146,9 +155,7 @@ public void testtSchemaMappingDetails() { @Tag("IntegrationTest") @Order(6) public void testListSchemaMappings() { - assertDoesNotThrow(() -> { - actions.ListSchemaMappings(); - }); + assertDoesNotThrow(actions::ListSchemaMappings); logger.info("Test 6 passed"); } @@ -157,7 +164,7 @@ public void testListSchemaMappings() { @Order(7) public void testLTagResources() { assertDoesNotThrow(() -> { - actions.tagEntityResource(mappingARN).join(); + actions.tagEntityResource(csvMappingARN).join(); }); logger.info("Test 7 passed"); } @@ -170,48 +177,11 @@ public void testLDeleteMapping() { logger.info("Wait 30 mins for the workflow to complete"); Thread.sleep(1800000); actions.deleteMatchingWorkflowAsync(workflowName).join(); + actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); + actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); + CloudFormationHelper.emptyS3Bucket(glueBucketName); + CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); }); logger.info("Test 8 passed"); } - - private static String getSecretValues() { - SecretsManagerClient secretClient = SecretsManagerClient.builder() - .region(Region.US_EAST_1) - .build(); - String secretName = "test/entity"; - - GetSecretValueRequest valueRequest = GetSecretValueRequest.builder() - .secretId(secretName) - .build(); - - GetSecretValueResponse valueResponse = secretClient.getSecretValue(valueRequest); - return valueResponse.secretString(); - } - - @Nested - @DisplayName("A class used to get test values from test/cognito (an AWS Secrets Manager secret)") - class SecretValues { - private String roleARN; - private String dataS3bucket; - - private String outputBucket; - - private String inputGlueTableArn; - - public String getRoleARN() { - return roleARN; - } - - public String getDataS3bucket() { - return dataS3bucket; - } - - public String getOutputBucket() { - return outputBucket; - } - - public String getInputGlueTableArn() { - return inputGlueTableArn; - } - } } diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index f7c4c616544..22bb26e21de 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -281,6 +281,7 @@ The following table describes the metadata used in this Basics Scenario. | `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | | `tagResource ` | entity_metadata.yaml | entity_TagResource | | `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | +| `deleteMapping ` | entity_metadata.yaml | entity_DeleteSchemaMapping | | `listMappingJobs ` | entity_metadata.yaml | entity_Hello | | `scenario` | entity_metadata.yaml | entity_Scenario | From b2d0b0fe33d6a0921d0d5e143edfe0bc736be40e Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:01:23 -0500 Subject: [PATCH 128/144] updated service level readme --- .../example_code/entityresolution/README.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 javav2/example_code/entityresolution/README.md diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md new file mode 100644 index 00000000000..fa5c9287dd5 --- /dev/null +++ b/javav2/example_code/entityresolution/README.md @@ -0,0 +1,123 @@ +# AWS Entity Resolution code examples for the SDK for Java 2.x + +## Overview + +Shows how to use the AWS SDK for Java 2.x to work with AWS Entity Resolution. + + + + +_AWS Entity Resolution helps organizations extract, link, and organize information from multiple data sources._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://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](https://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](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav2` folder. + + + + + +### Get started + +- [Hello AWS Entity Resolution](src/main/java/com/example/entity/HelloEntityResoultion.java#L19) (`listMatchingWorkflows`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](src/main/java/com/example/entity/scenario/EntityResScenario.java) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L305) +- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L333) +- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L183) +- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) +- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L250) +- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L223) +- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L136) +- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L276) +- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L413) +- [entityresolution_DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS Entity Resolution + +This example shows you how to get started using AWS Entity Resolution. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create Schema Mapping. +- Create an AWS Entity Resolution workflow. +- Start the matching job for the workflow. +- Get details for the matching job. +- Get Schema Mapping. +- List all Schema Mappings. +- Tag the Schema Mapping resource. +- Delete the AWS Entity Resolution Assets. + + + + + + + + + +### 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 `javav2` folder. + + + + + + +## Additional resources + +- [AWS Entity Resolution User Guide](https://docs.aws.amazon.com/entityresolution/latest/userguide/what-is-service.html) +- [AWS Entity Resolution API Reference](https://docs.aws.amazon.com/entityresolution/latest/apireference/Welcome.html) +- [SDK for Java 2.x AWS Entity Resolution reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/entityresolution/package-summary.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 From 48e09335a3c213d72b86ec0d534948074c45bcd7 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:05:29 -0500 Subject: [PATCH 129/144] updated service level readme --- .doc_gen/metadata/entityresolution_metadata.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index c25218b5164..9906a5edb0f 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -25,7 +25,7 @@ entityresolution_DeleteSchemaMapping: snippet_tags: - entityres.java2_delete_mappings.main services: - entityresolution: {entityresolution_DeleteSchemaMapping} + entityresolution: {DeleteSchemaMapping} entityresolution_TagEntityResource: languages: Java: @@ -159,4 +159,5 @@ entityresolution_Scenario: snippet_tags: - entityres.java2_actions.main services: - entityresolution: {} \ No newline at end of file + entityresolution: {} + \ No newline at end of file From ff7c784d6cd504e137eb2eb1c23e6b55e81ea060 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:08:38 -0500 Subject: [PATCH 130/144] updated service level readme --- javav2/example_code/entityresolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md index fa5c9287dd5..5ea507e27b2 100644 --- a/javav2/example_code/entityresolution/README.md +++ b/javav2/example_code/entityresolution/README.md @@ -49,12 +49,12 @@ Code excerpts that show you how to call individual service functions. - [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L333) - [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L183) - [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) +- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) - [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L250) - [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L223) - [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L136) - [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L276) - [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L413) -- [entityresolution_DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) From 99a4d1a6d32ae53c071b93632ae1356196c48e43 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 15:10:42 -0500 Subject: [PATCH 131/144] updated service level readme --- .doc_gen/metadata/entityresolution_metadata.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.doc_gen/metadata/entityresolution_metadata.yaml b/.doc_gen/metadata/entityresolution_metadata.yaml index 9906a5edb0f..b318b6c2a41 100644 --- a/.doc_gen/metadata/entityresolution_metadata.yaml +++ b/.doc_gen/metadata/entityresolution_metadata.yaml @@ -160,4 +160,3 @@ entityresolution_Scenario: - entityres.java2_actions.main services: entityresolution: {} - \ No newline at end of file From ded62092687033659b6226776fb662a2e19d7a8f Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 25 Feb 2025 18:24:21 -0500 Subject: [PATCH 132/144] updated exception hanlder to stop program if an exception is thrown --- .../entity/scenario/EntityResScenario.java | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 9899c69a34e..68248c12e02 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -111,7 +111,12 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); logger.info(csv); waitForInputToContinue(scanner); - actions.uploadInputData(glueBucketName, json, csv); + try { + actions.uploadInputData(glueBucketName, json, csv); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } logger.info("The JSON objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); logger.info(DASHES); @@ -135,7 +140,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The JSON schema mapping name is " + jsonSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } try { @@ -144,7 +149,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); } waitForInputToContinue(scanner); logger.info(DASHES); @@ -168,7 +173,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } waitForInputToContinue(scanner); @@ -181,7 +187,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -193,7 +200,8 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.getMatchingJobAsync(jobId, workflowName).join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.info("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } logger.info(DASHES); @@ -205,14 +213,20 @@ Amazon Web Services (AWS) that helps organizations extract, link, and jsonSchemaMappingArn = response.schemaArn(); logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { - logger.info("Error retrieving schema mapping: " + ce.getCause().getMessage()); + logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); + return; } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("6. List Schema Mappings."); - actions.ListSchemaMappings(); + try { + actions.ListSchemaMappings(); + } catch (CompletionException ce) { + logger.error("Error retrieving schema mapping: " + ce.getCause().getMessage()); + return; + } waitForInputToContinue(scanner); logger.info(DASHES); @@ -225,7 +239,13 @@ Amazon Web Services (AWS) that helps organizations extract, link, and In Entity Resolution, SchemaMapping and MatchingWorkflow can be tagged. For this example, the SchemaMapping is tagged. """); - actions.tagEntityResource(jsonSchemaMappingArn).join(); + try { + actions.tagEntityResource(jsonSchemaMappingArn).join(); + } catch (CompletionException ce) { + logger.error("Error tagging the resource: " + ce.getCause().getMessage()); + return; + } + waitForInputToContinue(scanner); logger.info(DASHES); @@ -280,6 +300,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); + return; } try { @@ -289,6 +310,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Both schema mappings were deleted successfully!"); } catch (RuntimeException e) { logger.error("Error deleting schema mapping: {}", e.getMessage()); + return; } waitForInputToContinue(scanner); @@ -304,6 +326,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); + return; } } else { From 91e43fd584cc9c3def81c5690e6112afb9edf2d2 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Wed, 26 Feb 2025 19:25:10 -0500 Subject: [PATCH 133/144] updated exception hanlder to stop program if an exception is thrown --- .../entity/scenario/EntityResActions.java | 492 +++++++++++------- .../entity/scenario/EntityResScenario.java | 141 ++++- 2 files changed, 425 insertions(+), 208 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 5c6e24c6016..04f84f66971 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -12,13 +12,17 @@ import software.amazon.awssdk.services.entityresolution.EntityResolutionAsyncClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowRequest; import software.amazon.awssdk.services.entityresolution.model.CreateMatchingWorkflowResponse; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowRequest; +import software.amazon.awssdk.services.entityresolution.model.DeleteMatchingWorkflowResponse; import software.amazon.awssdk.services.entityresolution.model.DeleteSchemaMappingRequest; +import software.amazon.awssdk.services.entityresolution.model.DeleteSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingRequest; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.InputSource; @@ -28,9 +32,12 @@ import software.amazon.awssdk.services.entityresolution.model.OutputSource; import software.amazon.awssdk.services.entityresolution.model.ResolutionTechniques; import software.amazon.awssdk.services.entityresolution.model.ResolutionType; +import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; import software.amazon.awssdk.services.entityresolution.model.SchemaAttributeType; import software.amazon.awssdk.services.entityresolution.model.SchemaInputAttribute; import software.amazon.awssdk.services.entityresolution.model.StartMatchingJobRequest; +import software.amazon.awssdk.services.entityresolution.model.TagResourceResponse; +import software.amazon.awssdk.services.entityresolution.model.ValidationException; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -41,6 +48,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { @@ -60,23 +68,23 @@ public static EntityResolutionAsyncClient getResolutionAsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); entityResolutionAsyncClient = EntityResolutionAsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return entityResolutionAsyncClient; } @@ -91,23 +99,23 @@ public static S3AsyncClient getS3AsyncClient() { */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() - .maxConcurrency(50) // Adjust as needed. - .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. - .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. - .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. - .build(); + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() - .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. - .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. - .retryStrategy(RetryMode.STANDARD) - .build(); + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); s3AsyncClient = S3AsyncClient.builder() - .region(Region.US_EAST_1) - .httpClient(httpClient) - .overrideConfiguration(overrideConfig) - .build(); + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); } return s3AsyncClient; } @@ -118,17 +126,32 @@ public static S3AsyncClient getS3AsyncClient() { * * @param schemaName the name of the schema to delete * @return a {@link CompletableFuture} that completes when the schema mapping is deleted successfully, - * or throws a {@link RuntimeException} if the deletion fails + * or throws a {@link RuntimeException} if the deletion fails */ - public CompletableFuture deleteSchemaMappingAsync(String schemaName) { + public CompletableFuture deleteSchemaMappingAsync(String schemaName) { DeleteSchemaMappingRequest request = DeleteSchemaMappingRequest.builder() .schemaName(schemaName) .build(); return getResolutionAsyncClient().deleteSchemaMapping(request) - .thenRun(() -> logger.info("Schema mapping '{}' deleted successfully.", schemaName)) - .exceptionally(ex -> { - throw new RuntimeException("Failed to delete schema mapping: " + schemaName, ex); + .whenComplete((response, exception) -> { + if (response != null) { + // Successfully deleted the schema mapping, log the success message. + logger.info("Schema mapping '{}' deleted successfully.", schemaName); + } else { + // Ensure exception is not null before accessing its cause. + if (exception == null) { + throw new CompletionException("An unknown error occurred while deleting the schema mapping.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The schema mapping was not found to delete: " + schemaName, cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to delete schema mapping: " + schemaName, exception); + } }); } // snippet-end:[entityres.java2_delete_mappings.main] @@ -140,14 +163,14 @@ public CompletableFuture deleteSchemaMappingAsync(String schemaName) { */ public void ListSchemaMappings() { ListSchemaMappingsRequest mappingsRequest = ListSchemaMappingsRequest.builder() - .build(); + .build(); ListSchemaMappingsPublisher paginator = getResolutionAsyncClient().listSchemaMappingsPaginator(mappingsRequest); // Iterate through the pages of results CompletableFuture future = paginator.subscribe(response -> { response.schemaList().forEach(schemaMapping -> - logger.info("Schema Mapping Name: " + schemaMapping.schemaName()) + logger.info("Schema Mapping Name: " + schemaMapping.schemaName()) ); }); @@ -157,7 +180,6 @@ public void ListSchemaMappings() { // snippet-end:[entityres.java2_list_mappings.main] // snippet-start:[entityres.java2_delete_matching_workflow.main] - /** * Asynchronously deletes a workflow with the specified name. * @@ -165,18 +187,29 @@ public void ListSchemaMappings() { * @return a {@link CompletableFuture} that completes when the workflow has been deleted * @throws RuntimeException if the deletion of the workflow fails */ - public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { + public CompletableFuture deleteMatchingWorkflowAsync(String workflowName) { DeleteMatchingWorkflowRequest request = DeleteMatchingWorkflowRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().deleteMatchingWorkflow(request) - .thenAccept(response -> { - // No response object, just log success - }) - .exceptionally(exception -> { - throw new RuntimeException("Failed to delete workflow: " + exception.getMessage(), exception); - }); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("{} was deleted", workflowName ); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while deleting the workflow.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The workflow to delete was not found.", cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to delete workflow: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_delete_matching_workflow.main] @@ -191,32 +224,42 @@ public CompletableFuture createSchemaMappingAsync(S List schemaAttributes = null; if (schemaName.startsWith("json")) { schemaAttributes = List.of( - SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), - SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), - SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build() + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build() ); } else { schemaAttributes = List.of( - SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), - SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), - SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build(), - SchemaInputAttribute.builder().fieldName("phone").type(SchemaAttributeType.PROVIDER_ID).subType("STRING").build() + SchemaInputAttribute.builder().matchKey("id").fieldName("id").type(SchemaAttributeType.UNIQUE_ID).build(), + SchemaInputAttribute.builder().matchKey("name").fieldName("name").type(SchemaAttributeType.NAME).build(), + SchemaInputAttribute.builder().matchKey("email").fieldName("email").type(SchemaAttributeType.EMAIL_ADDRESS).build(), + SchemaInputAttribute.builder().fieldName("phone").type(SchemaAttributeType.PROVIDER_ID).subType("STRING").build() ); } CreateSchemaMappingRequest request = CreateSchemaMappingRequest.builder() - .schemaName(schemaName) - .mappedInputFields(schemaAttributes) - .build(); + .schemaName(schemaName) + .mappedInputFields(schemaAttributes) + .build(); return getResolutionAsyncClient().createSchemaMapping(request) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("[{}] schema mapping Created Successfully!", schemaName); - } else { - throw new RuntimeException("Failed to create schema mapping: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("[{}] schema mapping Created Successfully!", schemaName); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while creating the schema mapping.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ConflictException) { + throw new CompletionException("A conflicting schema mapping already exists. Resolve conflicts before proceeding.", cause); } - }); + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to create schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_create_schema.main] @@ -226,29 +269,38 @@ public CompletableFuture createSchemaMappingAsync(S * * @param schemaName the name of the schema to retrieve the mapping for * @return a {@link CompletableFuture} that completes with the {@link GetSchemaMappingResponse} when the operation - * is complete + * is complete * @throws RuntimeException if the schema mapping retrieval fails */ public CompletableFuture getSchemaMappingAsync(String schemaName) { GetSchemaMappingRequest mappingRequest = GetSchemaMappingRequest.builder() - .schemaName(schemaName) - .build(); + .schemaName(schemaName) + .build(); return getResolutionAsyncClient().getSchemaMapping(mappingRequest) - .whenComplete((response, exception) -> { - if (response != null) { - response.mappedInputFields().forEach(attribute -> - logger.info("Attribute Name: " + attribute.fieldName() + - ", Attribute Type: " + attribute.type().toString())); - } else { - throw new RuntimeException("Failed to get schema mapping: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + response.mappedInputFields().forEach(attribute -> + logger.info("Attribute Name: " + attribute.fieldName() + + ", Attribute Type: " + attribute.type().toString())); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while getting schema mapping.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The requested schema mapping was not found.", cause); } - }); + + // Wrap other exceptions in a CompletionException with the message. + throw new CompletionException("Failed to get schema mapping: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_get_schema_mapping.main] // snippet-start:[entityres.java2_get_job.main] - /** * Asynchronously retrieves a matching job based on the provided job ID and workflow name. * @@ -256,20 +308,33 @@ public CompletableFuture getSchemaMappingAsync(String * @param workflowName the name of the workflow associated with the job * @return a {@link CompletableFuture} that completes when the job information is available or an exception occurs */ - public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { + public CompletableFuture getMatchingJobAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenAccept(response -> { + .whenComplete((response, exception) -> { + if (response != null) { + // Successfully fetched the matching job details, log the job status. logger.info("Job status: " + response.status()); logger.info("Job details: " + response.toString()); - }) - .exceptionally(ex -> { - throw new RuntimeException("Error fetching matching job: " + ex.getMessage(), ex); - }); + } else { + // Handle the case where there is an exception. + if (exception == null) { + throw new CompletionException("An unknown error occurred while fetching the matching job.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The requested job could not be found.", cause); + } + + // Wrap other exceptions in a CompletionException with the message. + throw new CompletionException("Error fetching matching job: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_get_job.main] @@ -280,168 +345,230 @@ public CompletableFuture getMatchingJobAsync(String jobId, String workflow * * @param workflowName the name of the workflow for which to start the matching job * @return a {@link CompletableFuture} that completes with the job ID of the started matching job, or an empty - * string if the operation fails + * string if the operation fails */ public CompletableFuture startMatchingJobAsync(String workflowName) { StartMatchingJobRequest jobRequest = StartMatchingJobRequest.builder() - .workflowName(workflowName) - .build(); + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().startMatchingJob(jobRequest) - .whenComplete((response, exception) -> { - if (response != null) { - // Get the job ID from the response - String jobId = response.jobId(); - logger.info("Job ID: " + jobId); - } else { - // Handle the exception if the response is null - throw new RuntimeException("Failed to start matching job: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + String jobId = response.jobId(); + logger.info("Job ID: " + jobId); + } else { + // Ensure exception is not null before accessing its cause. + if (exception == null) { + throw new CompletionException("An unknown error occurred while starting the job.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ConflictException) { + throw new CompletionException("The job is already running. Resolve conflicts before starting a new job.", cause); } - }) - .thenApply(response -> response != null ? response.jobId() : ""); + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to start the job: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response != null ? response.jobId() : ""); } // snippet-end:[entityres.java2_start_job.main] // snippet-start:[entityres.java2_check_matching_workflow.main] - /** * Checks the status of a workflow asynchronously. * * @param jobId the ID of the job to check * @param workflowName the name of the workflow to check * @return a CompletableFuture that resolves to a boolean value indicating whether the workflow has completed - * successfully + * successfully */ - public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { + public CompletableFuture checkWorkflowStatusCompleteAsync(String jobId, String workflowName) { GetMatchingJobRequest request = GetMatchingJobRequest.builder() - .jobId(jobId) - .workflowName(workflowName) - .build(); + .jobId(jobId) + .workflowName(workflowName) + .build(); return getResolutionAsyncClient().getMatchingJob(request) - .thenApply(response -> { - logger.info("\nJob status: " + response.status()); - return "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status())); - }) - .exceptionally(exception -> { - logger.info("Error checking workflow status: " + exception.getMessage()); - return false; - }); + .whenComplete((response, exception) -> { + if (response != null) { + // Process the response and log the job status. + logger.info("Job status: " + response.status()); + } else { + // Ensure exception is not null before accessing its cause. + if (exception == null) { + throw new CompletionException("An unknown error occurred while checking job status.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The requested resource was not found while checking the job status.", cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to check job status: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_check_matching_workflow.main] // snippet-start:[entityres.java2_create_matching_workflow.main] - /** * Creates an asynchronous CompletableFuture to manage the creation of a matching workflow. * - * @param roleARN the AWS IAM role ARN to be used for the workflow execution - * @param workflowName the name of the workflow to be created - * @param outputBucket the S3 bucket path where the workflow output will be stored - * @param jsonGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source + * @param roleARN the AWS IAM role ARN to be used for the workflow execution + * @param workflowName the name of the workflow to be created + * @param outputBucket the S3 bucket path where the workflow output will be stored + * @param jsonGlueTableArn the ARN of the Glue Data Catalog table to be used as the input source * @param jsonErSchemaMappingName the name of the schema to be used for the input source * @return a CompletableFuture that, when completed, will return the ARN of the created workflow */ public CompletableFuture createMatchingWorkflowAsync( - String roleARN - , String workflowName - , String outputBucket - , String jsonGlueTableArn - , String jsonErSchemaMappingName - , String csvGlueTableArn - , String csvErSchemaMappingName) { + String roleARN + , String workflowName + , String outputBucket + , String jsonGlueTableArn + , String jsonErSchemaMappingName + , String csvGlueTableArn + , String csvErSchemaMappingName) { InputSource jsonInputSource = InputSource.builder() - .inputSourceARN(jsonGlueTableArn) - .schemaName(jsonErSchemaMappingName) - .applyNormalization(false) - .build(); + .inputSourceARN(jsonGlueTableArn) + .schemaName(jsonErSchemaMappingName) + .applyNormalization(false) + .build(); InputSource csvInputSource = InputSource.builder() - .inputSourceARN(csvGlueTableArn) - .schemaName(csvErSchemaMappingName) - .applyNormalization(false) - .build(); + .inputSourceARN(csvGlueTableArn) + .schemaName(csvErSchemaMappingName) + .applyNormalization(false) + .build(); OutputAttribute idOutputAttribute = OutputAttribute.builder() - .name("id") - .build(); + .name("id") + .build(); OutputAttribute nameOutputAttribute = OutputAttribute.builder() - .name("name") - .build(); + .name("name") + .build(); OutputAttribute emailOutputAttribute = OutputAttribute.builder() - .name("email") - .build(); + .name("email") + .build(); OutputAttribute phoneOutputAttribute = OutputAttribute.builder() - .name("phone") - .build(); + .name("phone") + .build(); OutputSource outputSource = OutputSource.builder() - .outputS3Path("s3://" + outputBucket + "/eroutput") - .output(idOutputAttribute, nameOutputAttribute, emailOutputAttribute, phoneOutputAttribute) - .applyNormalization(false) - .build(); + .outputS3Path("s3://" + outputBucket + "/eroutput") + .output(idOutputAttribute, nameOutputAttribute, emailOutputAttribute, phoneOutputAttribute) + .applyNormalization(false) + .build(); ResolutionTechniques resolutionType = ResolutionTechniques.builder() - .resolutionType(ResolutionType.ML_MATCHING) - .build(); + .resolutionType(ResolutionType.ML_MATCHING) + .build(); CreateMatchingWorkflowRequest workflowRequest = CreateMatchingWorkflowRequest.builder() - .roleArn(roleARN) - .description("Created by using the AWS SDK for Java") - .workflowName(workflowName) - .inputSourceConfig(List.of(jsonInputSource, csvInputSource)) - .outputSourceConfig(List.of(outputSource)) - .resolutionTechniques(resolutionType) - .build(); + .roleArn(roleARN) + .description("Created by using the AWS SDK for Java") + .workflowName(workflowName) + .inputSourceConfig(List.of(jsonInputSource, csvInputSource)) + .outputSourceConfig(List.of(outputSource)) + .resolutionTechniques(resolutionType) + .build(); return getResolutionAsyncClient().createMatchingWorkflow(workflowRequest) - .whenComplete((response, exception) -> { - if (response != null) { - logger.info("Workflow created successfully."); - } else { - throw new RuntimeException("Failed to create workflow: " + exception.getMessage(), exception); + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Workflow created successfully."); + } else { + Throwable cause = exception.getCause(); + if (cause instanceof ValidationException) { + throw new CompletionException("Invalid request: Please check input parameters.", cause); } - }) - .thenApply(CreateMatchingWorkflowResponse::workflowArn); + + if (cause instanceof ConflictException) { + throw new CompletionException("A conflicting workflow already exists. Resolve conflicts before proceeding.", cause); + } + throw new CompletionException("Failed to create workflow: " + exception.getMessage(), exception); + } + }) + .thenApply(CreateMatchingWorkflowResponse::workflowArn); } // snippet-end:[entityres.java2_create_matching_workflow.main] // snippet-start:[entityres.java2_tag_resource.main] - /** * Tags the specified schema mapping ARN. * * @param schemaMappingARN the ARN of the schema mapping to tag */ - public CompletableFuture tagEntityResource(String schemaMappingARN) { + public CompletableFuture tagEntityResource(String schemaMappingARN) { Map tags = new HashMap<>(); tags.put("tag1", "tag1Value"); tags.put("tag2", "tag2Value"); TagResourceRequest request = TagResourceRequest.builder() - .resourceArn(schemaMappingARN) - .tags(tags) - .build(); + .resourceArn(schemaMappingARN) + .tags(tags) + .build(); return getResolutionAsyncClient().tagResource(request) - .thenAccept(response -> logger.info("Successfully tagged the resource.")) - .exceptionally(exception -> { - throw new RuntimeException("Failed to tag the resource: " + exception.getMessage(), exception); - }); + .whenComplete((response, exception) -> { + if (response != null) { + // Successfully tagged the resource, log the success message. + logger.info("Successfully tagged the resource."); + } else { + if (exception == null) { + throw new CompletionException("An unknown error occurred while tagging the resource.", null); + } + + Throwable cause = exception.getCause(); + if (cause instanceof ResourceNotFoundException) { + throw new CompletionException("The resource to tag was not found.", cause); + } + + // Wrap other AWS exceptions in a CompletionException. + throw new CompletionException("Failed to tag the resource: " + exception.getMessage(), exception); + } + }); } // snippet-end:[entityres.java2_tag_resource.main] - public CompletableFuture getJobInfo(String workflowName, String jobId){ - return getResolutionAsyncClient().getMatchingJob(b -> b - .workflowName(workflowName) - .jobId(jobId)) - .thenApply(response -> response.metrics()); + // snippet-start:[entityres.java2_job_info.main] + public CompletableFuture getJobInfo(String workflowName, String jobId) { + return getResolutionAsyncClient().getMatchingJob(b -> b + .workflowName(workflowName) + .jobId(jobId)) + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Job metrics fetched successfully for jobId: " + jobId); + } else { + Throwable cause = exception.getCause(); + + + if (cause instanceof ResourceNotFoundException) { + // Handle validation errors if needed + throw new CompletionException("Invalid request: Job id was not found.", cause); + } + + if (cause instanceof ConflictException) { + // Handle conflict errors if needed + throw new CompletionException("A conflicting request occurred. Resolve conflicts before proceeding.", cause); + } + // Generic failure case + throw new CompletionException("Failed to fetch job info: " + exception.getMessage(), exception); + } + }) + .thenApply(response -> response.metrics()); // Extract job metrics } + // snippet-end:[entityres.java2_job_info.main] + /** * Uploads data to an Amazon S3 bucket asynchronously. * @@ -456,28 +583,29 @@ public void uploadInputData(String bucketName, String jsonData, String csvData) // Upload JSON data. String jsonKey = "jsonData/data.json"; PutObjectRequest jsonUploadRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(jsonKey) - .contentType("application/json") - .build(); + .bucket(bucketName) + .key(jsonKey) + .contentType("application/json") + .build(); CompletableFuture jsonUploadResponse = getS3AsyncClient().putObject(jsonUploadRequest, AsyncRequestBody.fromString(jsonData)); // Upload CSV data. String csvKey = "csvData/data.csv"; PutObjectRequest csvUploadRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(csvKey) - .contentType("text/csv") - .build(); + .bucket(bucketName) + .key(csvKey) + .contentType("text/csv") + .build(); CompletableFuture csvUploadResponse = getS3AsyncClient().putObject(csvUploadRequest, AsyncRequestBody.fromString(csvData)); CompletableFuture.allOf(jsonUploadResponse, csvUploadResponse) - .whenComplete((result, ex) -> { - if (ex != null) { - throw new RuntimeException("Failed to upload files", ex); - } - }).join(); + .whenComplete((result, ex) -> { + if (ex != null) { + // Wrap an AWS exception. + throw new CompletionException("Failed to upload files", ex); + } + }).join(); } // snippet-end:[entityres.java2_actions.main] diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 68248c12e02..5a11e7d4529 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -4,12 +4,19 @@ package com.example.entity.scenario; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.entityresolution.model.AccessDeniedException; +import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.JobMetrics; +import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; +import software.amazon.awssdk.services.entityresolution.model.ThrottlingException; +import software.amazon.awssdk.services.entityresolution.model.ValidationException; import java.util.Map; import java.util.Scanner; @@ -28,16 +35,17 @@ public class EntityResScenario { private static String glueBucketName; private static String workflowName = "workflow-" + UUID.randomUUID(); + private static String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); + private static String jsonSchemaMappingArn = null; + private static String csvSchemaMappingName = "csv-" + UUID.randomUUID(); + private static String roleARN; + private static String csvGlueTableArn; + private static String jsonGlueTableArn; + private static Scanner scanner = new Scanner(System.in); + + private static EntityResActions actions = new EntityResActions(); public static void main(String[] args) throws InterruptedException { - String jsonSchemaMappingName = "jsonschema-" + UUID.randomUUID(); - String jsonSchemaMappingArn = null; - String csvSchemaMappingName = "csv-" + UUID.randomUUID(); - String roleARN; - String csvGlueTableArn; - String jsonGlueTableArn; - - EntityResActions actions = new EntityResActions(); - Scanner scanner = new Scanner(System.in); + logger.info("Welcome to the AWS Entity Resolution Scenario."); logger.info(""" AWS Entity Resolution is a fully-managed machine learning service provided by @@ -88,6 +96,16 @@ Amazon Web Services (AWS) that helps organizations extract, link, and jsonGlueTableArn = outputsMap.get(JSON_GLUE_TABLE_ARN_KEY); logger.info(DASHES); waitForInputToContinue(scanner); + + try { + runScenario(); + + } catch (Exception ce) { + Throwable cause = ce.getCause(); + logger.error("An exception happened: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + } + private static void runScenario() throws InterruptedException { /* This JSON is a valid input for the AWS Entity Resolution service. The JSON represents an array of three objects, each containing an "id", "name", and "email" @@ -115,9 +133,23 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.uploadInputData(glueBucketName, json, csv); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + + if (cause == null) { + logger.error("Failed to upload input data: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ResourceNotFoundException) { + logger.error("The S3 bucket could not be found: {}", cause.getMessage(), cause); + } else { + logger.error("Failed to upload input data: {}", cause.getMessage(), cause); + } + + // Always wrap checked exceptions in a CompletionException + throw (cause instanceof RuntimeException) ? (RuntimeException) cause + : new CompletionException("Unhandled checked exception during input data upload", cause); } - logger.info("The JSON objects have been uploaded to the S3 bucket."); + logger.info("The JSON and CSV objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); logger.info(DASHES); @@ -133,14 +165,27 @@ Amazon Web Services (AWS) that helps organizations extract, link, and In this example, the schema mapping lines up with the fields in the JSON ans CSV objects. That is, it contains these fields: id, name, and email. """); - waitForInputToContinue(scanner); try { CreateSchemaMappingResponse response = actions.createSchemaMappingAsync(jsonSchemaMappingName).join(); jsonSchemaMappingName = response.schemaName(); logger.info("The JSON schema mapping name is " + jsonSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create JSON schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + + if (cause == null) { + logger.error("Failed to create JSON schema mapping: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ConflictException) { + logger.error("Schema mapping conflict detected: {}", cause.getMessage(), cause); + } else { + logger.error("Unexpected error while creating schema mapping: {}", cause.getMessage(), cause); + } + + // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException + throw (cause instanceof RuntimeException) ? (RuntimeException) cause + : new CompletionException("Unhandled checked exception in schema mapping creation", cause); } try { @@ -149,7 +194,21 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create CSV schema mapping: " + (cause != null ? cause.getMessage() : ce.getMessage())); + + if (cause == null) { + logger.error("Failed to create CSV schema mapping: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ConflictException) { + logger.error("Schema mapping conflict detected: {}", cause.getMessage(), cause); + } else { + logger.error("Unexpected error while creating CSV schema mapping: {}", cause.getMessage(), cause); + } + + // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException + throw (cause instanceof RuntimeException) ? (RuntimeException) cause + : new CompletionException("Unhandled checked exception in schema mapping creation", cause); } waitForInputToContinue(scanner); logger.info(DASHES); @@ -173,8 +232,27 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to create workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; + + if (cause == null) { + // Log the exception and rethrow the CompletionException if no cause is found + logger.error("An unexpected error occurred: {}", ce.getMessage(), ce); + throw ce; + } + + if (cause instanceof ValidationException) { + logger.error("Validation error: {}", cause.getMessage(), cause); + } else if (cause instanceof ConflictException) { + logger.error("Workflow conflict detected: {}", cause.getMessage(), cause); + } else { + logger.error("Unexpected error: {}", ce.getMessage(), ce); + } + + // Rethrow as a RuntimeException if cause is a RuntimeException, else rethrow CompletionException + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new CompletionException("Unhandled exception occurred during workflow creation", cause); + } } waitForInputToContinue(scanner); @@ -187,8 +265,18 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; + + if (cause instanceof ConflictException) { + logger.error("Job conflict detected: {}", cause.getMessage(), cause); + } else if (cause instanceof RuntimeException) { + // Log and rethrow RuntimeException, ensuring it's captured in the first block + logger.error("Runtime error while starting the job: {}", cause.getMessage(), cause); + throw (RuntimeException) cause; // Rethrow RuntimeException for the first block to handle + } else { + logger.error("Unexpected error while starting the job: {}", ce.getMessage(), ce); + // For other checked exceptions, wrap them in a new CompletionException + throw new CompletionException("Unhandled checked exception while starting the job.", cause); + } } waitForInputToContinue(scanner); logger.info(DASHES); @@ -201,7 +289,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and } catch (CompletionException ce) { Throwable cause = ce.getCause(); logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; + throw ce; } logger.info(DASHES); @@ -214,7 +302,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); - return; + throw ce; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -225,7 +313,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.ListSchemaMappings(); } catch (CompletionException ce) { logger.error("Error retrieving schema mapping: " + ce.getCause().getMessage()); - return; + throw ce; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -243,7 +331,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and actions.tagEntityResource(jsonSchemaMappingArn).join(); } catch (CompletionException ce) { logger.error("Error tagging the resource: " + ce.getCause().getMessage()); - return; + throw ce; } waitForInputToContinue(scanner); @@ -291,7 +379,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and """); - logger.info("Do you want to delete the resources, including the workflow?"); + logger.info("Do you want to delete the resources, including the workflow? (y/n)"); String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { try { @@ -374,10 +462,11 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota // Check workflow status every 60 seconds. if (secondsElapsed % 60 == 0 || remainingTime <= 0) { - if (actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join()) { + GetMatchingJobResponse response = actions.checkWorkflowStatusCompleteAsync(jobId, workflowName).join(); + if (response != null && "SUCCEEDED".equalsIgnoreCase(String.valueOf(response.status()))) { logger.info(""); // Move to the next line after countdown. - logger.info("Countdown complete: Workflow is in SUCCEEDED state!"); - break; + logger.info("Countdown complete: Workflow is in Completed state!"); + break; // Break out of the loop if the status is "SUCCEEDED" } } From f0a81ec82e6090fc9be4004970b380766d4cb980 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Thu, 27 Feb 2025 09:28:35 -0500 Subject: [PATCH 134/144] updated the service level readme --- .../example_code/entityresolution/README.md | 20 +-- .../entity/scenario/EntityResActions.java | 14 -- .../entity/scenario/EntityResScenario.java | 137 +++++++++--------- .../basics/entity_resolution/SPECIFICATION.md | 3 +- 4 files changed, 77 insertions(+), 97 deletions(-) diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md index 5ea507e27b2..d0a7195d8a5 100644 --- a/javav2/example_code/entityresolution/README.md +++ b/javav2/example_code/entityresolution/README.md @@ -45,16 +45,16 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. -- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L305) -- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L333) -- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L183) -- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) -- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L115) -- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L250) -- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L223) -- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L136) -- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L276) -- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L413) +- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L377) +- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L415) +- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L216) +- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L182) +- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L123) +- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L303) +- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L266) +- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) +- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L340) +- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L502) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 04f84f66971..09f55e8e8c4 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -321,7 +321,6 @@ public CompletableFuture getMatchingJobAsync(String jobI logger.info("Job status: " + response.status()); logger.info("Job details: " + response.toString()); } else { - // Handle the case where there is an exception. if (exception == null) { throw new CompletionException("An unknown error occurred while fetching the matching job.", null); } @@ -358,7 +357,6 @@ public CompletableFuture startMatchingJobAsync(String workflowName) { String jobId = response.jobId(); logger.info("Job ID: " + jobId); } else { - // Ensure exception is not null before accessing its cause. if (exception == null) { throw new CompletionException("An unknown error occurred while starting the job.", null); } @@ -531,8 +529,6 @@ public CompletableFuture tagEntityResource(String schemaMap if (cause instanceof ResourceNotFoundException) { throw new CompletionException("The resource to tag was not found.", cause); } - - // Wrap other AWS exceptions in a CompletionException. throw new CompletionException("Failed to tag the resource: " + exception.getMessage(), exception); } }); @@ -549,19 +545,9 @@ public CompletableFuture getJobInfo(String workflowName, String jobI logger.info("Job metrics fetched successfully for jobId: " + jobId); } else { Throwable cause = exception.getCause(); - - if (cause instanceof ResourceNotFoundException) { - // Handle validation errors if needed throw new CompletionException("Invalid request: Job id was not found.", cause); } - - if (cause instanceof ConflictException) { - // Handle conflict errors if needed - throw new CompletionException("A conflicting request occurred. Resolve conflicts before proceeding.", cause); - } - - // Generic failure case throw new CompletionException("Failed to fetch job info: " + exception.getMessage(), exception); } }) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 5a11e7d4529..0fca53b9663 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -44,6 +44,7 @@ public class EntityResScenario { private static Scanner scanner = new Scanner(System.in); private static EntityResActions actions = new EntityResActions(); + public static void main(String[] args) throws InterruptedException { logger.info("Welcome to the AWS Entity Resolution Scenario."); @@ -105,6 +106,7 @@ Amazon Web Services (AWS) that helps organizations extract, link, and logger.error("An exception happened: " + (cause != null ? cause.getMessage() : ce.getMessage())); } } + private static void runScenario() throws InterruptedException { /* This JSON is a valid input for the AWS Entity Resolution service. @@ -136,18 +138,12 @@ private static void runScenario() throws InterruptedException { if (cause == null) { logger.error("Failed to upload input data: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ResourceNotFoundException) { - logger.error("The S3 bucket could not be found: {}", cause.getMessage(), cause); - } else { - logger.error("Failed to upload input data: {}", cause.getMessage(), cause); + logger.error("Failed to upload input data as the resource was not found: {}", cause.getMessage(), cause); } - - // Always wrap checked exceptions in a CompletionException - throw (cause instanceof RuntimeException) ? (RuntimeException) cause - : new CompletionException("Unhandled checked exception during input data upload", cause); + return; } logger.info("The JSON and CSV objects have been uploaded to the S3 bucket."); waitForInputToContinue(scanner); @@ -174,7 +170,6 @@ private static void runScenario() throws InterruptedException { if (cause == null) { logger.error("Failed to create JSON schema mapping: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ConflictException) { @@ -182,10 +177,7 @@ private static void runScenario() throws InterruptedException { } else { logger.error("Unexpected error while creating schema mapping: {}", cause.getMessage(), cause); } - - // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException - throw (cause instanceof RuntimeException) ? (RuntimeException) cause - : new CompletionException("Unhandled checked exception in schema mapping creation", cause); + return; } try { @@ -194,10 +186,8 @@ private static void runScenario() throws InterruptedException { logger.info("The CSV schema mapping name is " + csvSchemaMappingName); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - if (cause == null) { logger.error("Failed to create CSV schema mapping: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ConflictException) { @@ -205,10 +195,7 @@ private static void runScenario() throws InterruptedException { } else { logger.error("Unexpected error while creating CSV schema mapping: {}", cause.getMessage(), cause); } - - // Ensure that all exceptions are properly wrapped in a RuntimeException or CompletionException - throw (cause instanceof RuntimeException) ? (RuntimeException) cause - : new CompletionException("Unhandled checked exception in schema mapping creation", cause); + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -228,15 +215,16 @@ private static void runScenario() throws InterruptedException { """); waitForInputToContinue(scanner); try { - String workflowArn = actions.createMatchingWorkflowAsync(roleARN, workflowName, glueBucketName, jsonGlueTableArn, jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + String workflowArn = actions.createMatchingWorkflowAsync( + roleARN, workflowName, glueBucketName, jsonGlueTableArn, + jsonSchemaMappingName, csvGlueTableArn, csvSchemaMappingName).join(); + logger.info("The workflow ARN is: " + workflowArn); } catch (CompletionException ce) { Throwable cause = ce.getCause(); if (cause == null) { - // Log the exception and rethrow the CompletionException if no cause is found logger.error("An unexpected error occurred: {}", ce.getMessage(), ce); - throw ce; } if (cause instanceof ValidationException) { @@ -244,18 +232,12 @@ private static void runScenario() throws InterruptedException { } else if (cause instanceof ConflictException) { logger.error("Workflow conflict detected: {}", cause.getMessage(), cause); } else { - logger.error("Unexpected error: {}", ce.getMessage(), ce); - } - - // Rethrow as a RuntimeException if cause is a RuntimeException, else rethrow CompletionException - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else { - throw new CompletionException("Unhandled exception occurred during workflow creation", cause); + logger.error("Unexpected error: {}", cause.getMessage(), cause); } + return; } - waitForInputToContinue(scanner); + waitForInputToContinue(scanner); logger.info(DASHES); logger.info("3. Start the matching job of the " + workflowName + " workflow."); waitForInputToContinue(scanner); @@ -265,18 +247,12 @@ private static void runScenario() throws InterruptedException { logger.info("The matching job was successfully started."); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - if (cause instanceof ConflictException) { logger.error("Job conflict detected: {}", cause.getMessage(), cause); - } else if (cause instanceof RuntimeException) { - // Log and rethrow RuntimeException, ensuring it's captured in the first block - logger.error("Runtime error while starting the job: {}", cause.getMessage(), cause); - throw (RuntimeException) cause; // Rethrow RuntimeException for the first block to handle } else { logger.error("Unexpected error while starting the job: {}", ce.getMessage(), ce); - // For other checked exceptions, wrap them in a new CompletionException - throw new CompletionException("Unhandled checked exception while starting the job.", cause); } + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -288,8 +264,12 @@ private static void runScenario() throws InterruptedException { actions.getMatchingJobAsync(jobId, workflowName).join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); - logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); - throw ce; + if (cause instanceof ResourceNotFoundException) { + logger.error("The matching job not found: {}", cause.getMessage(), cause); + } else { + logger.error("Failed to start matching job: " + (cause != null ? cause.getMessage() : ce.getMessage())); + } + return; } logger.info(DASHES); @@ -301,8 +281,13 @@ private static void runScenario() throws InterruptedException { jsonSchemaMappingArn = response.schemaArn(); logger.info("Schema mapping ARN is " + jsonSchemaMappingArn); } catch (CompletionException ce) { - logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); - throw ce; + Throwable cause = ce.getCause(); + if (cause instanceof ResourceNotFoundException) { + logger.error("Schema mapping not found: {}", cause.getMessage(), cause); + } else { + logger.error("Error retrieving the specific schema mapping: " + ce.getCause().getMessage()); + } + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -312,8 +297,8 @@ private static void runScenario() throws InterruptedException { try { actions.ListSchemaMappings(); } catch (CompletionException ce) { - logger.error("Error retrieving schema mapping: " + ce.getCause().getMessage()); - throw ce; + logger.error("Error retrieving schema mappings: " + ce.getCause().getMessage()); + return; } waitForInputToContinue(scanner); logger.info(DASHES); @@ -331,7 +316,7 @@ private static void runScenario() throws InterruptedException { actions.tagEntityResource(jsonSchemaMappingArn).join(); } catch (CompletionException ce) { logger.error("Error tagging the resource: " + ce.getCause().getMessage()); - throw ce; + return; } waitForInputToContinue(scanner); @@ -352,32 +337,42 @@ private static void runScenario() throws InterruptedException { if (viewAns.equalsIgnoreCase("y")) { logger.info("You selected to view the Entity Resolution Workflow results."); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); - JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); - logger.info("Number of input records: {}", metrics.inputRecords()); - logger.info("Number of match ids: {}", metrics.matchIDs()); - logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); - logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); - logger.info(""" - - The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: - - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - - Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; - For example 'Bob Smith Jr.' compared to 'Bob Smith'. - The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, - the confidence level is lower for the differing email addresses. - - """); + try { + JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); + logger.info("Number of input records: {}", metrics.inputRecords()); + logger.info("Number of match ids: {}", metrics.matchIDs()); + logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); + logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); + logger.info(""" + + The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s + + Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; + For example 'Bob Smith Jr.' compared to 'Bob Smith'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, + the confidence level is lower for the differing email addresses. + + """); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof ResourceNotFoundException) { + logger.error("The job not found: {}", cause.getMessage(), cause); + } else { + logger.error("Error retrieving job information: " + ce.getCause().getMessage()); + } + return; + } logger.info("Do you want to delete the resources, including the workflow? (y/n)"); String delAns = scanner.nextLine().trim(); diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 22bb26e21de..f65ed408c12 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -38,8 +38,7 @@ The AWS Entity Resolution Basics scenario executes the following operations. should be resolved and matched. The method `createMatchingWorkflow` is called. - Exception Handling: Check to see if a `ConflictException` is thrown, which - is thrown if the matching workflow already exists. If so, display the - message and end the program. + is thrown if the matching workflow already exists. ALso check to see if a `ValidationException` is thrown. If so, display the message and end the program. 3. **Start Matching Workflow**: - Description: Initiates a matching workflow by calling the From 8f6e9b4a03252e7b9097f227efe3ee7bb052ae53 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 09:56:10 -0500 Subject: [PATCH 135/144] rolled in review comments --- .../src/main/resources/TODO.md | 8 ------- .../com/myorg/EntityResolutionCdkApp.java | 24 +------------------ 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 javav2/example_code/entityresolution/src/main/resources/TODO.md diff --git a/javav2/example_code/entityresolution/src/main/resources/TODO.md b/javav2/example_code/entityresolution/src/main/resources/TODO.md deleted file mode 100644 index 8e3963dca2a..00000000000 --- a/javav2/example_code/entityresolution/src/main/resources/TODO.md +++ /dev/null @@ -1,8 +0,0 @@ -# Suggestions to improve the scenario - -Need to delete the schema mapping when you delete the workflow. - -Use two input data sources, since that is what a customer would do at a minimum. The input data for the scenario should contain records that do -and don't match. Make the second data source in CSV. - -When the job completes, display the results from the S3 bucket--both success and error. \ No newline at end of file diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java index e7428abe1da..ef25c7d2c34 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkApp.java @@ -14,29 +14,7 @@ public static void main(final String[] args) { App app = new App(); new EntityResolutionCdkStack(app, "EntityResolutionCdkStack", StackProps.builder() - // If you don't specify 'env', this stack will be environment-agnostic. - // Account/Region-dependent features and context lookups will not work, - // but a single synthesized template can be deployed anywhere. - - // Uncomment the next block to specialize this stack for the AWS Account - // and Region that are implied by the current CLI configuration. - /* - .env(Environment.builder() - .account(System.getenv("CDK_DEFAULT_ACCOUNT")) - .region(System.getenv("CDK_DEFAULT_REGION")) - .build()) - */ - - // Uncomment the next block if you know exactly what Account and Region you - // want to deploy the stack to. - /* - .env(Environment.builder() - .account("123456789012") - .region("us-east-1") - .build()) - */ - - // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html .build()); app.synth(); From e0174a2cd3fb93b45817d5d175fb1063acdbe0ab Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 10:14:57 -0500 Subject: [PATCH 136/144] rolled in review comments --- .../com/myorg/EntityResolutionCdkStack.java | 23 ------------------- .../basics/entity_resolution/SPECIFICATION.md | 12 ++++++++++ 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java index d46eeddf636..21127b7ae88 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -49,29 +49,6 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build(); // 3. Create a Glue table referencing the S3 bucket -/* CfnTable glueTable = CfnTable.Builder.create(this, "GlueTable") - .catalogId(this.getAccount()) - .databaseName(glueDatabase.getRef()) // Ensure Glue Table references the database correctly - .tableInput(CfnTable.TableInputProperty.builder() - .name("entity_resolution") // Fixed table name reference - .tableType("EXTERNAL_TABLE") - .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() - .columns(List.of( - CfnTable.ColumnProperty.builder().name("id").type("string").build(), // Fixed: id is a string, - CfnTable.ColumnProperty.builder().name("name").type("string").build(), - CfnTable.ColumnProperty.builder().name("email").type("string").build() - )) - .location("s3://" + glueDataBucket.getBucketName() + "/data/") // Append subpath for data - .inputFormat("org.apache.hadoop.mapred.TextInputFormat") - .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") - .serdeInfo(CfnTable.SerdeInfoProperty.builder() - .serializationLibrary("org.openx.data.jsonserde.JsonSerDe") // Set JSON SerDe - .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON - .build()) - .build()) - .build()) - .build();*/ - final CfnTable jsonErGlueTable = createGlueTable(jsonGlueTableName , jsonGlueTableName , glueDatabase.getRef() diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index f65ed408c12..ee64d45e962 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -108,6 +108,18 @@ With Entity Resolution, organizations can unlock the value of their data, improve decision-making, and enhance customer experiences by having a reliable, comprehensive view of their key entities. +Enter 'c' followed by to continue: +c + +To prepare the AWS resources needed for this scenario application, the next step uploads +a CloudFormation template whose resulting stack creates the following resources: + - An AWS Glue Data Catalog table + - An AWS IAM role + - An AWS S3 bucket + - An AWS Entity Resolution Schema + +It can take a couple minutes for the Stack to finish creating the resources. + Enter 'c' followed by to continue: c From 5e7d20a4886b59a641810912c058f6c1537ab64a Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 15:33:29 -0500 Subject: [PATCH 137/144] rolled in review comments --- .../entity/scenario/CloudFormationHelper.java | 2 +- .../entity/scenario/EntityResScenario.java | 156 +++++---- .../com/myorg/EntityResolutionCdkStack.java | 87 ++--- .../basics/entity_resolution/SPECIFICATION.md | 305 +++++++++++++----- 4 files changed, 360 insertions(+), 190 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java index 9de189ea437..12f48a586bd 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/CloudFormationHelper.java @@ -100,7 +100,7 @@ public static void deployCloudFormationStack(String stackName) { } }).join(); } else { - logger.info("{} stack already exists", CFN_TEMPLATE); + logger.info("{} stack already exists", stackName); } } diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 0fca53b9663..0c2e9e58cc8 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -5,6 +5,7 @@ import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; import software.amazon.awssdk.services.entityresolution.model.AccessDeniedException; import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; @@ -17,6 +18,7 @@ import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; import software.amazon.awssdk.services.entityresolution.model.ThrottlingException; import software.amazon.awssdk.services.entityresolution.model.ValidationException; +import software.amazon.awssdk.services.s3.model.S3Exception; import java.util.Map; import java.util.Scanner; @@ -115,18 +117,18 @@ private static void runScenario() throws InterruptedException { Entity Resolution service. */ String json = """ - {"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} - {"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} - {"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} + {"id":"1","name":"Jane Doe","email":"jane.doe@example.com"} + {"id":"2","name":"John Doe","email":"john.doe@example.com"} + {"id":"3","name":"Jorge Souza","email":"jorge_souza@example.com"} """; logger.info("Upload the following JSON objects to the {} S3 bucket.", glueBucketName); logger.info(json); String csv = """ id,name,email,phone - 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 - 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 - 3,Charlie Black,charlie.black@company.com,345-567-1234 - 7,Jane E. Doe,jane_doe@company.com,111-222-3333 + 1,Jane B.,Doe,jane.doe@example.com,555-876-9846 + 2,John Doe Jr.,john.doe@example.com,555-654-3210 + 3,María García,maría_garcia@company.com,555-567-1234 + 4,Mary Major,mary_major@company.com,555-222-3333 """; logger.info("Upload the following CSV data to the {} S3 bucket.", glueBucketName); logger.info(csv); @@ -158,7 +160,7 @@ private static void runScenario() throws InterruptedException { and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. - In this example, the schema mapping lines up with the fields in the JSON ans CSV objects. That is, + In this example, the schema mapping lines up with the fields in the JSON and CSV objects. That is, it contains these fields: id, name, and email. """); try { @@ -328,8 +330,8 @@ private static void runScenario() throws InterruptedException { You cannot view the result of the workflow that is in a running state. In order to view the results, you need to wait for the workflow that we started in step 3 to complete. - If you choose not to wait, you cannot view the results or delete the workflow. You would have to - perform both tasks manually in the AWS Management Console. + If you choose not to wait, you cannot view the results. You can perform + this task manually in the AWS Management Console. This can take up to 30 mins (y/n). """); @@ -343,27 +345,26 @@ private static void runScenario() throws InterruptedException { logger.info("Number of match ids: {}", metrics.matchIDs()); logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); + logger.info("The following represents the actual output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); logger.info(""" - - The output of the machinelearning-based matching job is a CSV file in the S3 bucket. The following is a sample of the output: - + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 \s - - Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; - For example 'Bob Smith Jr.' compared to 'Bob Smith'. - The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, - the confidence level is lower for the differing email addresses. - + + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable Mary Major mary_major@company.com, 555-222-3333 4 ec05e7a55a0d4319b86da0a65286118f000040 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 María García maría_garcia@company.com 555-567-1234 3 201ed8241ec04f9aa7fcfd962220580500001369367187456 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 1 Jane Doe jane.doe@example.com 1 895c3a439dc44a298663d52c08635e1a0000434359738368 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 1 Jane B.Doe jane.doe@example.com 1 69c2b2190c60427c8f5a2daa7ce5d45b00001463856467968 \s + arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.8914204 2 John Doe john.doe@example.com 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s + arn:aws:glue:us-east-1::xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.8914204 2 John Doe Jr. john.doe@example.com 555-654-3210 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s + + Note that each of the last 2 records are considered a match even though the 'name' differs between the records; + For example 'John Doe Jr.' compared to 'John Doe'. + The confidence level is a value between 0 and 1, where 1 indicates a perfect match. + """); + } catch (CompletionException ce) { Throwable cause = ce.getCause(); if (cause instanceof ResourceNotFoundException) { @@ -373,49 +374,64 @@ private static void runScenario() throws InterruptedException { } return; } + } - logger.info("Do you want to delete the resources, including the workflow? (y/n)"); - String delAns = scanner.nextLine().trim(); - if (delAns.equalsIgnoreCase("y")) { - try { - actions.deleteMatchingWorkflowAsync(workflowName).join(); - logger.info("Workflow deleted successfully!"); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.info("Failed to delete workflow: " + (cause != null ? cause.getMessage() : ce.getMessage())); - return; - } + waitForInputToContinue(scanner); + logger.info(DASHES); - try { - // Delete both schema mappings. - actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); - actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); - logger.info("Both schema mappings were deleted successfully!"); - } catch (RuntimeException e) { - logger.error("Error deleting schema mapping: {}", e.getMessage()); - return; - } + logger.info(DASHES); + logger.info("9. Do you want to delete the resources, including the workflow? (y/n)"); + logger.info(""" + You cannot delete the workflow that is in a running state. + In order to delete the workflow, you need to wait for the workflow to complete. + + You can delete the workflow manually in the AWS Management Console at a later time. + + If you already waited for the workflow to complete in the previous step, + the workflow is completed and you can delete it. + + If the workflow is not completed, this can take up to 30 mins (y/n). + """); + String delAns = scanner.nextLine().trim(); + if (delAns.equalsIgnoreCase("y")) { + try { + countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + actions.deleteMatchingWorkflowAsync(workflowName).join(); + logger.info("Workflow deleted successfully!"); + } catch (CompletionException ce) { + logger.info("Error deleting the workflow: {} ", ce.getMessage()); + return; + } - waitForInputToContinue(scanner); - logger.info(DASHES); - logger.info(""" - Now we delete the CloudFormation stack, which deletes - the resources that were created at the beginning - """); - waitForInputToContinue(scanner); - logger.info(DASHES); - try { - deleteResources(); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - logger.error("Failed to delete Glue Table: {}", cause != null ? cause.getMessage() : ce.getMessage()); - return; - } + try { + // Delete both schema mappings. + actions.deleteSchemaMappingAsync(jsonSchemaMappingName).join(); + actions.deleteSchemaMappingAsync(csvSchemaMappingName).join(); + logger.info("Both schema mappings were deleted successfully!"); + } catch (CompletionException ce) { + logger.error("Error deleting schema mapping: {}", ce.getMessage()); + return; + } - } else { - logger.info("You can delete the Workflow later in the AWS Management console."); + waitForInputToContinue(scanner); + logger.info(DASHES); + logger.info(""" + Now we delete the CloudFormation stack, which deletes + the resources that were created at the beginning of this scenario. + """); + waitForInputToContinue(scanner); + logger.info(DASHES); + try { + deleteCloudFormationStack(); + } catch (RuntimeException e) { + logger.error("Failed to delete the stack: {}", e.getMessage()); + return; } + + } else { + logger.info("You can delete the AWS resources in the AWS Management Console."); } + waitForInputToContinue(scanner); logger.info(DASHES); @@ -472,10 +488,16 @@ public static void countdownWithWorkflowCheck(EntityResActions actions, int tota } } - private static void deleteResources() { - CloudFormationHelper.emptyS3Bucket(glueBucketName); - CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); - logger.info("Resources deleted successfully!"); + private static void deleteCloudFormationStack() { + try { + CloudFormationHelper.emptyS3Bucket(glueBucketName); + CloudFormationHelper.destroyCloudFormationStack(STACK_NAME); + logger.info("Resources deleted successfully!"); + } catch (CloudFormationException e) { + throw new RuntimeException("Failed to delete CloudFormation stack: " + e.getMessage(), e); + } catch (S3Exception e) { + throw new RuntimeException("Failed to empty S3 bucket: " + e.getMessage(), e); + } } } // snippet-end:[entityres.java2_scenario.main] \ No newline at end of file diff --git a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java index 21127b7ae88..c8871872d69 100644 --- a/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java +++ b/resources/cdk/entityresolution_resources/src/main/java/com/myorg/EntityResolutionCdkStack.java @@ -35,10 +35,10 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St String uniqueId = UUID.randomUUID().toString().replace("-", ""); // Remove dashes to ensure compatibility Bucket erBucket = Bucket.Builder.create(this, "ErBucket") - .bucketName("erbucket" + uniqueId) - .versioned(false) - .removalPolicy(RemovalPolicy.DESTROY) - .build(); + .bucketName("erbucket" + uniqueId) + .versioned(false) + .removalPolicy(RemovalPolicy.DESTROY) + .build(); // 2. Create a Glue database CfnDatabase glueDatabase = CfnDatabase.Builder.create(this, "GlueDatabase") @@ -50,21 +50,21 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St // 3. Create a Glue table referencing the S3 bucket final CfnTable jsonErGlueTable = createGlueTable(jsonGlueTableName - , jsonGlueTableName - , glueDatabase.getRef() - , Map.of("id", "string", "name", "string", "email", "string") - , "s3://" + erBucket.getBucketName() + "/jsonData/" - , "org.openx.data.jsonserde.JsonSerDe"); + , jsonGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string") + , "s3://" + erBucket.getBucketName() + "/jsonData/" + , "org.openx.data.jsonserde.JsonSerDe"); // Ensure Glue Table is created after the Database jsonErGlueTable.addDependency(glueDatabase); final CfnTable csvErGlueTable = createGlueTable(csvGlueTableName - , csvGlueTableName - , glueDatabase.getRef() - , Map.of("id", "string", "name", "string", "email", "string", "phone", "string") - , "s3://" + erBucket.getBucketName() + "/csvData/" - , "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe"); + , csvGlueTableName + , glueDatabase.getRef() + , Map.of("id", "string", "name", "string", "email", "string", "phone", "string") + , "s3://" + erBucket.getBucketName() + "/csvData/" + , "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe"); // Ensure Glue Table is created after the Database csvErGlueTable.addDependency(glueDatabase); @@ -100,9 +100,9 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build()); new CfnOutput(this, "CsvErGlueTableArn", CfnOutputProps.builder() - .value(createGlueTableArn(csvErGlueTable, csvGlueTableName)) - .description("The ARN of the CSV Glue Table") - .build()); + .value(createGlueTableArn(csvErGlueTable, csvGlueTableName)) + .description("The ARN of the CSV Glue Table") + .build()); new CfnOutput(this, "GlueDataBucketName", CfnOutputProps.builder() .value(erBucket.getBucketName()) // Outputs the bucket name @@ -110,41 +110,42 @@ public EntityResolutionCdkStack(final Construct scope, final String id, final St .build()); } - CfnTable createGlueTable(String id, String tableName, String databaseRef, Map schemaMap, String dataLocation, String serializationLib){ + CfnTable createGlueTable(String id, String tableName, String databaseRef, Map schemaMap, String dataLocation, String serializationLib) { return CfnTable.Builder.create(this, id) - .catalogId(this.getAccount()) - .databaseName(databaseRef) // Ensure Glue Table references the database correctly - .tableInput(CfnTable.TableInputProperty.builder() - .name(tableName) // Fixed table name reference - .tableType("EXTERNAL_TABLE") - .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() - .columns(createColumns(schemaMap)) - .location(dataLocation) // Append subpath for data - .inputFormat("org.apache.hadoop.mapred.TextInputFormat") - .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") - .serdeInfo(CfnTable.SerdeInfoProperty.builder() - .serializationLibrary(serializationLib) // Set JSON SerDe - .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON - .build()) - .build()) + .catalogId(this.getAccount()) + .databaseName(databaseRef) // Ensure Glue Table references the database correctly + .tableInput(CfnTable.TableInputProperty.builder() + .name(tableName) // Fixed table name reference + .tableType("EXTERNAL_TABLE") + .storageDescriptor(CfnTable.StorageDescriptorProperty.builder() + .columns(createColumns(schemaMap)) + .location(dataLocation) // Append subpath for data + .inputFormat("org.apache.hadoop.mapred.TextInputFormat") + .outputFormat("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat") + .serdeInfo(CfnTable.SerdeInfoProperty.builder() + .serializationLibrary(serializationLib) // Set JSON SerDe + .parameters(Map.of("serialization.format", "1")) // Optional: Set the format for JSON .build()) - .build(); + .build()) + .build()) + .build(); } + List createColumns(Map schemaMap) { return schemaMap.entrySet().stream() - .map(entry -> CfnTable.ColumnProperty.builder() - .name(entry.getKey()) - .type(entry.getValue()) - .build()) - .toList(); + .map(entry -> CfnTable.ColumnProperty.builder() + .name(entry.getKey()) + .type(entry.getValue()) + .build()) + .toList(); } String createGlueTableArn(CfnTable glueTable, String glueTableName) { return String.format("arn:aws:glue:%s:%s:table/%s/%s" - , this.getRegion() - , this.getAccount() - , glueTable.getDatabaseName() - , glueTableName + , this.getRegion() + , this.getAccount() + , glueTable.getDatabaseName() + , glueTableName ); } } diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index ee64d45e962..9725de70998 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -55,36 +55,45 @@ The AWS Entity Resolution Basics scenario executes the following operations. thrown, which indicates that the workflow cannot be found. If the exception is thrown, display the message and end the program. -5. **List Matching Workflows**: - - Description: Lists all matching workflows created within the account by - calling the `listMatchingWorkflows` method. - - Exception Handling: Check to see if an `CompletionException` is thrown. If - so, display the message and end the program. - -6. **Get Schema Mapping**: +5. **Get Schema Mapping**: - Description: Returns the `SchemaMapping` of a given name by calling the `getSchemaMapping` method. - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program. +6. **List Matching Workflows**: + - Description: Lists all matching workflows created within the account by + calling the `listMatchingWorkflows` method. + - Exception Handling: Check to see if an `CompletionException` is thrown. If + so, display the message and end the program. + 7. **Tag Resource**: - Description: Adds tags associated with an AWS Entity Resolution resource by calling the`tagResource` method. - Exception Handling: Check to see if a `ResourceNotFoundException` is thrown. If so, display the message and end the program -8. **Delete Matching Workflow**: - - Description: Deletes a specified matching workflow by calling the - `deleteMatchingWorkflow` method. - - Exception Handling: Check to see if an `ConflictException` is thrown. If +8. **View the results of the AWS Entity Resolution Workflow**: + - Description: View the workflow results by calling the + `getMatchingJob` method. + - Exception Handling: Check to see if an `ResourceNotFoundException` is thrown. If so, display the message and end the program. +9. **Delete the AWS resources**: + - Description: Delete the AWS resouces including the workflow and schema mappings by calling the + `deleteMatchingWorkflow` and `deleteSchemaMapping` methods. + - Exception Handling: Check to see if an `ResourceNotFoundException` is thrown. If + so, display the message and end the program. + - Finally delete the CloudFormation Stack by calling these method: + - CloudFormationHelper.emptyS3Bucket(glueBucketName); + - CloudFormationHelper.destroyCloudFormationStack + ### Program execution The following shows the output of the AWS Entity Resolution Basics scenario in the console. ``` -Welcome to the AWS Entity Resolution Scenario. +Welcome to the AWS Entity Resolution Scenario. AWS Entity Resolution is a fully-managed machine learning service provided by Amazon Web Services (AWS) that helps organizations extract, link, and organize information from multiple data sources. It leverages natural @@ -108,16 +117,20 @@ With Entity Resolution, organizations can unlock the value of their data, improve decision-making, and enhance customer experiences by having a reliable, comprehensive view of their key entities. + Enter 'c' followed by to continue: c +Continuing with the program... +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- To prepare the AWS resources needed for this scenario application, the next step uploads a CloudFormation template whose resulting stack creates the following resources: - - An AWS Glue Data Catalog table - - An AWS IAM role - - An AWS S3 bucket - - An AWS Entity Resolution Schema - +- An AWS Glue Data Catalog table +- An AWS IAM role +- An AWS S3 bucket +- An AWS Entity Resolution Schema + It can take a couple minutes for the Stack to finish creating the resources. @@ -125,36 +138,33 @@ Enter 'c' followed by to continue: c Continuing with the program... +Generating resources... +Stack creation requested, ARN is arn:aws:cloudformation:us-east-1:814548047983:stack/EntityResolutionCdkStack/858988e0-f604-11ef-916b-0affc298c80f +Stack created successfully -------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -Upload the JSON to the glue-5ffb912c3d534e8493bac675c2a3196d S3 bucket if it does not exist -[ - { - "id": "1", - "name": "Alice Johnson", - "email": "alice.johnson@example.com" - }, - { - "id": "2", - "name": "Bob Smith", - "email": "bob.smith@example.com" - }, - { - "id": "3", - "name": "Charlie Black", - "email": "charlie.black@example.com" - } -] + +Enter 'c' followed by to continue: +c +Continuing with the program... + +Upload the following JSON objects to the erbucketf684533d2680435fa99d24b1bdaf5179 S3 bucket. +{"id":"1","name":"Jane Doe","email":"jane.doe@example.com"} +{"id":"2","name":"John Doe","email":"john.doe@example.com"} +{"id":"3","name":"Jorge Souza","email":"jorge_souza@example.com"} + +Upload the following CSV data to the erbucketf684533d2680435fa99d24b1bdaf5179 S3 bucket. +id,name,email,phone +1,Jane B.,Doe,jane.doe@example.com,555-876-9846 +2,John Doe Jr.,john.doe@example.com,555-654-3210 +3,María García,maría_garcia@company.com,555-567-1234 +4,Mary Major,mary_major@company.com,555-222-3333 Enter 'c' followed by to continue: c Continuing with the program... -SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". -SLF4J: Defaulting to no-operation (NOP) logger implementation -SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. -The JSON exists in glue-5ffb912c3d534e8493bac675c2a3196d +The JSON and CSV objects have been uploaded to the S3 bucket. Enter 'c' followed by to continue: c @@ -163,21 +173,19 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- 1. Create Schema Mapping -Entity Resolution Schema Mapping aligns and integrates data from +Entity Resolution schema mapping aligns and integrates data from multiple sources by identifying and matching corresponding entities like customers or products. It unifies schemas, resolves conflicts, and uses machine learning to link related entities, enabling a consolidated, accurate view for improved data quality and decision-making. -In this example, the schema mapping lines up with the fields in the JSON. That is, +In this example, the schema mapping lines up with the fields in the JSON and CSV objects. That is, it contains these fields: id, name, and email. - -Enter 'c' followed by to continue: -c -Continuing with the program... - -Schema Mapping Created Successfully! +[jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2] schema mapping Created Successfully! +The JSON schema mapping name is jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 +[csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c] schema mapping Created Successfully! +The CSV schema mapping name is csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c Enter 'c' followed by to continue: c @@ -192,7 +200,9 @@ customers or products. Using techniques like schema mapping, data profiling, and machine learning algorithms, it evaluates attributes like names or emails to detect duplicates or relationships, even with variations or inconsistencies. -The workflow outputs consolidated, de-duplicated data, +The workflow outputs consolidated, de-duplicated data. + +We will use the machine learning-based matching technique. Enter 'c' followed by to continue: @@ -200,20 +210,20 @@ c Continuing with the program... Workflow created successfully. -The workflow ARN is: arn:aws:entityresolution:us-east-1:814548047983:matchingworkflow/MyMatchingWorkflow450 +The workflow ARN is: arn:aws:entityresolution:us-east-1:814548047983:matchingworkflow/workflow-39216b7f-f00b-4896-84ae-cd7edcfc7872 Enter 'c' followed by to continue: c Continuing with the program... -------------------------------------------------------------------------------- -3. Start the matching job of the MyMatchingWorkflow450 workflow. +3. Start the matching job of the workflow-39216b7f-f00b-4896-84ae-cd7edcfc7872 workflow. Enter 'c' followed by to continue: c Continuing with the program... -Job ID: ec2dbd1717624b2b806ed93a04c20049 +Job ID: f25d2707729646a4af27874d991e22c5 The matching job was successfully started. Enter 'c' followed by to continue: @@ -222,32 +232,70 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -4. Get details for job ec2dbd1717624b2b806ed93a04c20049 +4. While the matching job is running, let's look at other API methods. First, let's get details for job f25d2707729646a4af27874d991e22c5 Enter 'c' followed by to continue: c Continuing with the program... -Job status: QUEUED -Job details: GetMatchingJobResponse(JobId=ec2dbd1717624b2b806ed93a04c20049, StartTime=2025-01-30T18:37:57.475Z, Status=QUEUED) +Job status: RUNNING +Job details: GetMatchingJobResponse(JobId=f25d2707729646a4af27874d991e22c5, StartTime=2025-02-28T18:49:14.921Z, Status=RUNNING) -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -5. Get Schema Mapping. +5. Get the schema mapping for the JSON data. Enter 'c' followed by to continue: c Continuing with the program... Attribute Name: id, Attribute Type: UNIQUE_ID -Attribute Name: name, Attribute Type: STRING -Attribute Name: email, Attribute Type: STRING -Schema mapping retrieval completed. +Attribute Name: name, Attribute Type: NAME +Attribute Name: email, Attribute Type: EMAIL_ADDRESS +Schema mapping ARN is arn:aws:entityresolution:us-east-1:814548047983:schemamapping/jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 + +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- 6. List Schema Mappings. +Schema Mapping Name: csv-33f8e392-74e7-4a08-900a-652b94f86250 +Schema Mapping Name: csv-3b68e38b-1d5c-4836-bfc7-92ac7339e5c7 +Schema Mapping Name: csv-4f547deb-56c1-4923-9119-556bc43df08d +Schema Mapping Name: csv-6fe8bbc3-ebb5-4800-ab49-a89f75a87905 +Schema Mapping Name: csv-812ecad3-3175-49c3-93a5-d3175396d6e7 +Schema Mapping Name: csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c +Schema Mapping Name: csv-90a464e1-f050-422c-8f5f-0726541a5858 +Schema Mapping Name: csv-ebad3e3d-27be-4ed4-ae35-7401265e57bd +Schema Mapping Name: csv-f752d395-857b-4106-b2f2-85e1da5e3040 +Schema Mapping Name: jsonschema-363dc915-0540-406e-8d3f-4f1435e0b942 +Schema Mapping Name: jsonschema-5b1ad3e1-a840-4c4f-b791-5e9e1893fe7e +Schema Mapping Name: jsonschema-8623e0ec-bb8c-4fe2-a998-609eae08d84d +Schema Mapping Name: jsonschema-93d5fd04-f10e-4274-a181-489bea7b92db +Schema Mapping Name: jsonschema-b1653c13-ce77-471d-a3d5-ae4877216a74 +Schema Mapping Name: jsonschema-c09b3414-384c-4e3d-90c8-61e48abde04d +Schema Mapping Name: jsonschema-d9a6edc0-a9bd-4553-bb71-fbf0d6064ef9 +Schema Mapping Name: jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 +Schema Mapping Name: jsonschema-f0a259e0-f4e5-493a-bfd5-32740d2fa24d +Schema Mapping Name: schema2135 +Schema Mapping Name: schema435 +Schema Mapping Name: schema455 +Schema Mapping Name: schema456 +Schema Mapping Name: schema4648 +Schema Mapping Name: schema4720 +Schema Mapping Name: schema4848 +Schema Mapping Name: schema6758 +Schema Mapping Name: schema8775 +Schema Mapping Name: schemaName100 + +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -7. Tag the schema450resource. +7. Tag the jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2 resource. Tags can help you organize and categorize your Entity Resolution resources. You can also use them to scope user permissions by granting a user permission to access or change only resources with certain tag values. @@ -256,14 +304,72 @@ the SchemaMapping is tagged. Successfully tagged the resource. +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -8. Delete the AWS Entity Resolution Workflow. -You cannot delete a workflow that is in a running state. -Would you like to wait for the workflow to complete. +8. View the results of the AWS Entity Resolution Workflow. +You cannot view the result of the workflow that is in a running state. +In order to view the results, you need to wait for the workflow that we started in step 3 to complete. + +If you choose not to wait, you cannot view the results. You can perform +this task manually in the AWS Management Console. + This can take up to 30 mins (y/n). -n +y +You selected to view the Entity Resolution Workflow results. +29:01Job status: RUNNING +28:01Job status: RUNNING +27:01Job status: RUNNING +26:01Job status: RUNNING +25:01Job status: RUNNING +24:01Job status: RUNNING +23:01Job status: RUNNING +22:01Job status: RUNNING +21:01Job status: RUNNING +20:01Job status: RUNNING +19:01Job status: RUNNING +18:01Job status: RUNNING +17:01Job status: RUNNING +16:01Job status: RUNNING +15:01Job status: RUNNING +14:01Job status: RUNNING +13:01Job status: RUNNING +12:01Job status: RUNNING +11:01Job status: RUNNING +10:01Job status: RUNNING +09:01Job status: RUNNING +08:01Job status: RUNNING +07:01Job status: SUCCEEDED + +Countdown complete: Workflow is in Completed state! +Job metrics fetched successfully for jobId: f25d2707729646a4af27874d991e22c5 +Number of input records: 7 +Number of match ids: 6 +Number of records not processed: 0 +Number of total records processed: 7 +The following explains the output data generated by the Entity Resolution workflow. The output data is stored in the erbucketf684533d2680435fa99d24b1bdaf5179 bucket. + + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- --------------------------------------------------- + InputSourceARN ConfidenceLevel id name email phone RecordId MatchID + ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- --------------------------------------------------- + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 7 Jane E. Doe jane_doe@company.com 111-222-3333 7 036298535ed6471ebfc358fc76e1f51200006472446402560 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.90523 2 Bob Smith Jr. bob.smith@example.com 987-654-3210 2 6ae2d360d6594089837eafc31b20f31600003506806140928 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.90523 2 Bob Smith bob.smith@example.com 2 6ae2d360d6594089837eafc31b20f31600003506806140928 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.89398956 1 Alice B. Johnson alice.johnson@example.com 746-876-9846 1 34a5075b289247efa1847ab292ed677400009137438953472 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.89398956 1 Alice Johnson alice.johnson@example.com 1 34a5075b289247efa1847ab292ed677400009137438953472 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 Charlie Black charlie.black@company.com 345-567-1234 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 + arn:aws:glue:region:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.605295 3 Charlie Black charlie.black@example.com 3 92c8ef3f68b34948a3af998d700ed02700002146028888064 + +Note that each of the last 3 pairs of records are considered a match even though the 'name' or 'email' differ between the records; +For example 'Bob Smith Jr.' compared to 'Bob Smith'. +The confidence level is a value between 0 and 1, where 1 indicates a perfect match. In the last pair of matched records, +the confidence level is lower for the differing email addresses. + + Enter 'c' followed by to continue: c @@ -271,9 +377,50 @@ Continuing with the program... -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -This concludes the AWS Entity Resolution scenario. +9. Do you want to delete the resources, including the workflow? (y/n) +You cannot delete the workflow that is in a running state. +In order to delete the workflow, you need to wait for the workflow to complete. + +You can delete the workflow manually in the AWS Management Console at a later time. + +If you already waited for the workflow to complete in the previous step, +the workflow is completed and you can delete it. + +If the workflow is not completed, this can take up to 30 mins (y/n). + +y +workflow-39216b7f-f00b-4896-84ae-cd7edcfc7872 was deleted +Workflow deleted successfully! +Schema mapping 'jsonschema-ef86075e-cf5e-4bb1-be50-e0f19743ddb2' deleted successfully. +Schema mapping 'csv-8d05576d-66bb-4fcf-a29c-8c3de57ce48c' deleted successfully. +Both schema mappings were deleted successfully! + +Enter 'c' followed by to continue: +c +Continuing with the program... + +-------------------------------------------------------------------------------- +Now we delete the CloudFormation stack, which deletes +the resources that were created at the beginning of this scenario. + + +Enter 'c' followed by to continue: +c +Continuing with the program... + -------------------------------------------------------------------------------- +Delete stack requested .... +Stack deleted successfully. +Resources deleted successfully! + +Enter 'c' followed by to continue: +c +Continuing with the program... +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +This concludes the AWS Entity Resolution scenario. +-------------------------------------------------------------------------------- ``` @@ -281,20 +428,20 @@ This concludes the AWS Entity Resolution scenario. The following table describes the metadata used in this Basics Scenario. -| action | metadata file | metadata key | -|-------------------------|------------------------|-------------------------------| -| `createWorkflow` | entity_metadata.yaml | entity_CreateWorkflow | -| `createSchemaMapping` | entity_metadata.yaml | entity_CreateMapping | -| `startMatchingJob` | entity_metadata.yaml | entity_StartMatchingJob | -| `getMatchingJob` | entity_metadata.yaml | entity_GetMatchingJob | -| `listMatchingWorkflows` | entity_metadata.yaml | entity_ListMatchingWorkflows | -| `getSchemaMapping` | entity_metadata.yaml | entity_GetSchemaMapping | -| `listSchemaMappings` | entity_metadata.yaml | entity_ListSchemaMappings | -| `tagResource ` | entity_metadata.yaml | entity_TagResource | -| `deleteWorkflow ` | entity_metadata.yaml | entity_DeleteWorkflow | -| `deleteMapping ` | entity_metadata.yaml | entity_DeleteSchemaMapping | -| `listMappingJobs ` | entity_metadata.yaml | entity_Hello | -| `scenario` | entity_metadata.yaml | entity_Scenario | +| action | metadata file | metadata key | +|------------------------|----------------------------------|--------------------------------------| +| `createWorkflow` | entityresolution_metadata.yaml |entityresolution_CreateWorkflow | +| `createSchemaMapping` | entityresolution_metadata.yaml |entityresolution_CreateMapping | +| `startMatchingJob` | entityresolution_metadata.yaml |entityresolution_StartMatchingJob | +| `getMatchingJob` | entityresolution_metadata.yaml |entityresolution_GetMatchingJob | +| `listMatchingWorkflows`| entityresolution_metadata.yaml |entityresolution_ListMatchingWorkflows| +| `getSchemaMapping` | entityresolution_metadata.yaml |entityresolution_GetSchemaMapping | +| `listSchemaMappings` | entityresolution_metadata.yaml |entityresolution_ListSchemaMappings | +| `tagResource ` | entityresolution_metadata.yaml |entityresolution_TagResource | +| `deleteWorkflow ` | entityresolution_metadata.yaml |entityresolution_DeleteWorkflow | +| `deleteMapping ` | entityresolution_metadata.yaml |entityresolution_DeleteSchemaMapping | +| `listMappingJobs ` | entityresolution_metadata.yaml |entityresolution_Hello | +| `scenario` | entityresolution_metadata.yaml |entityresolution_Scenario | From ee82f839c37b66d59a20c8d83d9de9cf1f03136a Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 16:31:28 -0500 Subject: [PATCH 138/144] rolled in review comments --- .../example/entity/scenario/EntityResScenario.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index 0c2e9e58cc8..ea2e2a68a77 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -3,23 +3,17 @@ package com.example.entity.scenario; - -import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; -import software.amazon.awssdk.services.entityresolution.model.AccessDeniedException; import software.amazon.awssdk.services.entityresolution.model.ConflictException; import software.amazon.awssdk.services.entityresolution.model.CreateSchemaMappingResponse; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.entityresolution.model.GetMatchingJobResponse; import software.amazon.awssdk.services.entityresolution.model.GetSchemaMappingResponse; import software.amazon.awssdk.services.entityresolution.model.JobMetrics; import software.amazon.awssdk.services.entityresolution.model.ResourceNotFoundException; -import software.amazon.awssdk.services.entityresolution.model.ThrottlingException; import software.amazon.awssdk.services.entityresolution.model.ValidationException; import software.amazon.awssdk.services.s3.model.S3Exception; - import java.util.Map; import java.util.Scanner; import java.util.UUID; @@ -336,9 +330,11 @@ private static void runScenario() throws InterruptedException { This can take up to 30 mins (y/n). """); String viewAns = scanner.nextLine().trim(); + boolean isComplete = false; if (viewAns.equalsIgnoreCase("y")) { logger.info("You selected to view the Entity Resolution Workflow results."); countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + isComplete = true; try { JobMetrics metrics = actions.getJobInfo(workflowName, jobId).join(); logger.info("Number of input records: {}", metrics.inputRecords()); @@ -395,7 +391,9 @@ private static void runScenario() throws InterruptedException { String delAns = scanner.nextLine().trim(); if (delAns.equalsIgnoreCase("y")) { try { - countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + if (!isComplete) { + countdownWithWorkflowCheck(actions, 1800, jobId, workflowName); + } actions.deleteMatchingWorkflowAsync(workflowName).join(); logger.info("Workflow deleted successfully!"); } catch (CompletionException ce) { From 56f703f4b66b36b7b1d7e275be26591e1e981c33 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Fri, 28 Feb 2025 16:39:34 -0500 Subject: [PATCH 139/144] rolled in review comments --- .../basics/entity_resolution/SPECIFICATION.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scenarios/basics/entity_resolution/SPECIFICATION.md b/scenarios/basics/entity_resolution/SPECIFICATION.md index 9725de70998..1f0880688f4 100644 --- a/scenarios/basics/entity_resolution/SPECIFICATION.md +++ b/scenarios/basics/entity_resolution/SPECIFICATION.md @@ -428,20 +428,20 @@ This concludes the AWS Entity Resolution scenario. The following table describes the metadata used in this Basics Scenario. -| action | metadata file | metadata key | -|------------------------|----------------------------------|--------------------------------------| -| `createWorkflow` | entityresolution_metadata.yaml |entityresolution_CreateWorkflow | -| `createSchemaMapping` | entityresolution_metadata.yaml |entityresolution_CreateMapping | -| `startMatchingJob` | entityresolution_metadata.yaml |entityresolution_StartMatchingJob | -| `getMatchingJob` | entityresolution_metadata.yaml |entityresolution_GetMatchingJob | -| `listMatchingWorkflows`| entityresolution_metadata.yaml |entityresolution_ListMatchingWorkflows| -| `getSchemaMapping` | entityresolution_metadata.yaml |entityresolution_GetSchemaMapping | -| `listSchemaMappings` | entityresolution_metadata.yaml |entityresolution_ListSchemaMappings | -| `tagResource ` | entityresolution_metadata.yaml |entityresolution_TagResource | -| `deleteWorkflow ` | entityresolution_metadata.yaml |entityresolution_DeleteWorkflow | -| `deleteMapping ` | entityresolution_metadata.yaml |entityresolution_DeleteSchemaMapping | -| `listMappingJobs ` | entityresolution_metadata.yaml |entityresolution_Hello | -| `scenario` | entityresolution_metadata.yaml |entityresolution_Scenario | +| action | metadata file | metadata key | +|------------------------|--------------------------------|--------------------------------------------| +| `createWorkflow` | entityresolution_metadata.yaml |entityresolution_CreateMatchingWorkflow | +| `createSchemaMapping` | entityresolution_metadata.yaml |entityresolution_CreateSchemaMapping | +| `startMatchingJob` | entityresolution_metadata.yaml |entityresolution_StartMatchingJob | +| `getMatchingJob` | entityresolution_metadata.yaml |entityresolution_GetMatchingJob | +| `listMatchingWorkflows`| entityresolution_metadata.yaml |entityresolution_ListMatchingWorkflows | +| `getSchemaMapping` | entityresolution_metadata.yaml |entityresolution_GetSchemaMapping | +| `listSchemaMappings` | entityresolution_metadata.yaml |entityresolution_ListSchemaMappings | +| `tagResource ` | entityresolution_metadata.yaml |entityresolution_TagEntityResource | +| `deleteWorkflow ` | entityresolution_metadata.yaml |ntityresolution_DeleteMatchingWorkflow | +| `deleteMapping ` | entityresolution_metadata.yaml |entityresolution_DeleteSchemaMapping | +| `listMappingJobs ` | entityresolution_metadata.yaml |entityresolution_Hello | +| `scenario` | entityresolution_metadata.yaml |entityresolution_Scenario | From 389794e36ccb0bfc23bb80c8796b1ffac08b1118 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Sat, 1 Mar 2025 15:48:12 -0500 Subject: [PATCH 140/144] rolled in review comments --- .../java/com/example/entity/scenario/EntityResScenario.java | 2 +- .../entityresolution/src/main/resources/data.csv | 5 ----- .../entityresolution/src/main/resources/data.json | 3 --- 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 javav2/example_code/entityresolution/src/main/resources/data.csv delete mode 100644 javav2/example_code/entityresolution/src/main/resources/data.json diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index ea2e2a68a77..cb6067b1662 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -341,7 +341,7 @@ private static void runScenario() throws InterruptedException { logger.info("Number of match ids: {}", metrics.matchIDs()); logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); - logger.info("The following represents the actual output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); + logger.info("The following represents the output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); logger.info(""" ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s diff --git a/javav2/example_code/entityresolution/src/main/resources/data.csv b/javav2/example_code/entityresolution/src/main/resources/data.csv deleted file mode 100644 index 3ec062e335d..00000000000 --- a/javav2/example_code/entityresolution/src/main/resources/data.csv +++ /dev/null @@ -1,5 +0,0 @@ - id,name,email,phone - 1,Alice B. Johnson,alice.johnson@example.com,746-876-9846 - 2,Bob Smith Jr.,bob.smith@example.com,987-654-3210 - 3,Charlie Black,charlie.black@company.com,345-567-1234 - 7,Jane E. Doe,jane_doe@company.com,111-222-3333 \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/resources/data.json b/javav2/example_code/entityresolution/src/main/resources/data.json deleted file mode 100644 index 0375ab4e2be..00000000000 --- a/javav2/example_code/entityresolution/src/main/resources/data.json +++ /dev/null @@ -1,3 +0,0 @@ -{"id":"1","name":"Alice Johnson","email":"alice.johnson@example.com"} -{"id":"2","name":"Bob Smith","email":"bob.smith@example.com"} -{"id":"3","name":"Charlie Black","email":"charlie.black@example.com"} \ No newline at end of file From 4554696c5d9822009661afbb9102deea3545d434 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 4 Mar 2025 15:42:32 -0500 Subject: [PATCH 141/144] rolled in review comments --- javav2/example_code/entityresolution/pom.xml | 11 ++ .../entity/scenario/EntityResActions.java | 167 +++++++++++++++++- .../entity/scenario/EntityResScenario.java | 15 +- 3 files changed, 179 insertions(+), 14 deletions(-) diff --git a/javav2/example_code/entityresolution/pom.xml b/javav2/example_code/entityresolution/pom.xml index 19684620c48..a70292a446b 100644 --- a/javav2/example_code/entityresolution/pom.xml +++ b/javav2/example_code/entityresolution/pom.xml @@ -80,10 +80,21 @@ software.amazon.awssdk entityresolution + + com.opencsv + opencsv + 5.7.1 + software.amazon.awssdk s3 + + + org.fusesource.jansi + jansi + 2.4.0 + software.amazon.awssdk netty-nio-client diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 09f55e8e8c4..00c06d5554b 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -3,7 +3,11 @@ package com.example.entity.scenario; +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvException; +import org.fusesource.jansi.AnsiConsole; import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; @@ -40,18 +44,30 @@ import software.amazon.awssdk.services.entityresolution.model.ValidationException; import software.amazon.awssdk.services.entityresolution.paginators.ListSchemaMappingsPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.services.entityresolution.model.TagResourceRequest; + +import java.io.IOException; +import java.io.StringReader; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.fusesource.jansi.Ansi.ansi; // snippet-start:[entityres.java2_actions.main] public class EntityResActions { + + private static final String PREFIX = "eroutput/"; private static final Logger logger = LoggerFactory.getLogger(EntityResActions.class); private static EntityResolutionAsyncClient entityResolutionAsyncClient; @@ -594,5 +610,152 @@ public void uploadInputData(String bucketName, String jsonData, String csvData) }).join(); } -// snippet-end:[entityres.java2_actions.main] -} \ No newline at end of file + + /** + * Finds the latest file in the S3 bucket that starts with "run-" in any depth of subfolders + */ + private CompletableFuture findLatestMatchingFile(String bucketName) { + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucketName) + .prefix(PREFIX) // Searches within the given folder + .build(); + + return getS3AsyncClient().listObjectsV2(request) + .thenApply(response -> response.contents().stream() + .map(S3Object::key) + .filter(key -> key.matches(".*?/run-[0-9a-zA-Z\\-]+")) // Matches files like run-XXXXX in any subfolder + .max(String::compareTo) // Gets the latest file + .orElse(null)) + .whenComplete((result, exception) -> { + if (exception == null) { + if (result != null) { + logger.info("Latest matching file found: " + result); + } else { + logger.info("No matching files found."); + } + } else { + throw new CompletionException("Failed to find latest matching file: " + exception.getMessage(), exception); + } + }); + } + + /** + * Prints the data located in the file in the S3 bucket that starts with "run-" in any depth of subfolders + */ + public void printData(String bucketName) { + try { + // Find the latest file with "run-" prefix in any depth of subfolders. + String s3Key = findLatestMatchingFile(bucketName).join(); + if (s3Key == null) { + logger.error("No matching files found in S3."); + return; + } + + logger.info("Downloading file: " + s3Key); + + // Read CSV file as String. + String csvContent = readCSVFromS3Async(bucketName, s3Key).join(); + if (csvContent.isEmpty()) { + logger.error("File is empty."); + return; + } + + // Process CSV content. + List records = parseCSV(csvContent); + printTable(records); + + } catch (RuntimeException | IOException | CsvException e) { + logger.error("Error processing CSV file from S3: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Reads a CSV file from S3 and returns it as a String. + */ + private static CompletableFuture readCSVFromS3Async(String bucketName, String s3Key) { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(); + + // Initiating the asynchronous request to get the file as bytes + return getS3AsyncClient().getObject(getObjectRequest, AsyncResponseTransformer.toBytes()) + .thenApply(responseBytes -> responseBytes.asUtf8String()) // Convert bytes to UTF-8 string + .whenComplete((result, exception) -> { + if (exception != null) { + throw new CompletionException("Failed to read CSV from S3: " + exception.getMessage(), exception); + } else { + logger.info("Successfully fetched CSV file content from S3."); + } + }); + } + + /** + * Parses CSV content from a String into a list of records. + */ + private static List parseCSV(String csvContent) throws IOException, CsvException { + try (CSVReader csvReader = new CSVReader(new StringReader(csvContent))) { + return csvReader.readAll(); + } + } + + /** + * Prints the given CSV data in a formatted table + */ + private static void printTable(List records) { + if (records.isEmpty()) { + logger.info("No records found."); + return; + } + + String[] headers = records.get(0); + List rows = records.subList(1, records.size()); + + // Determine column widths dynamically based on longest content + int[] columnWidths = new int[headers.length]; + for (int i = 0; i < headers.length; i++) { + final int columnIndex = i; + int maxWidth = Math.max(headers[i].length(), rows.stream() + .map(row -> row.length > columnIndex ? row[columnIndex].length() : 0) + .max(Integer::compareTo) + .orElse(0)); + columnWidths[i] = Math.min(maxWidth, 25); // Limit max width for better readability + } + + // Enable ANSI Console for colored output + AnsiConsole.systemInstall(); + + // Print table header + logger.info(String.valueOf(ansi().fgYellow().a("=== CSV Data from S3 ===").reset())); + printRow(headers, columnWidths, true); + + // Print rows + rows.forEach(row -> printRow(row, columnWidths, false)); + + // Restore console to normal + AnsiConsole.systemUninstall(); + } + + private static void printRow(String[] row, int[] columnWidths, boolean isHeader) { + String border = IntStream.range(0, columnWidths.length) + .mapToObj(i -> "-".repeat(columnWidths[i] + 2)) + .collect(Collectors.joining("+", "+", "+")); + + if (isHeader) { + logger.info(border); + } + + logger.info("|"); + for (int i = 0; i < columnWidths.length; i++) { + String cell = (i < row.length && row[i] != null) ? row[i] : ""; + logger.info(" %-" + columnWidths[i] + "s |", isHeader ? ansi().fgBrightBlue().a(cell).reset() : cell); + } + System.out.println(); + + if (isHeader) { + logger.info(border); + } + } +} +// snippet-end:[entityres.java2_actions.main] \ No newline at end of file diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java index cb6067b1662..75f7dfc26f4 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResScenario.java @@ -342,19 +342,10 @@ private static void runScenario() throws InterruptedException { logger.info("Number of records not processed: {}", metrics.recordsNotProcessed()); logger.info("Number of total records processed: {}", metrics.totalRecordsProcessed()); logger.info("The following represents the output data generated by the Entity Resolution workflow based on the JSON and CSV input data. The output data is stored in the {} bucket.", glueBucketName); + actions.printData(glueBucketName); + logger.info(""" - - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - InputSourceARN ConfidenceLevel id name email phone RecordId MatchID \s - ------------------------------------------------------------------------------ ----------------- ---- ------------------ --------------------------- -------------- ---------- ---------------------------------------------------\s - - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable Mary Major mary_major@company.com, 555-222-3333 4 ec05e7a55a0d4319b86da0a65286118f000040 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.605295 3 María García maría_garcia@company.com 555-567-1234 3 201ed8241ec04f9aa7fcfd962220580500001369367187456 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 1 Jane Doe jane.doe@example.com 1 895c3a439dc44a298663d52c08635e1a0000434359738368 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 1 Jane B.Doe jane.doe@example.com 1 69c2b2190c60427c8f5a2daa7ce5d45b00001463856467968 \s - arn:aws:glue:us-east-1:xxxxxxxxxxxx:table/entity_resolution_db/jsongluetable 0.8914204 2 John Doe john.doe@example.com 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s - arn:aws:glue:us-east-1::xxxxxxxxxxxx:table/entity_resolution_db/csvgluetable 0.8914204 2 John Doe Jr. john.doe@example.com 555-654-3210 2 fbeda81b4c72429382c064b20cd592ff00001386547056640 \s - + Note that each of the last 2 records are considered a match even though the 'name' differs between the records; For example 'John Doe Jr.' compared to 'John Doe'. The confidence level is a value between 0 and 1, where 1 indicates a perfect match. From abb4cdef1811819432f3a27e3aedeb2a4e055e64 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 4 Mar 2025 16:20:37 -0500 Subject: [PATCH 142/144] updated the example --- .../example/entity/scenario/EntityResActions.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java index 00c06d5554b..b29a3cbec84 100644 --- a/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java +++ b/javav2/example_code/entityresolution/src/main/java/com/example/entity/scenario/EntityResActions.java @@ -700,12 +700,15 @@ private static List parseCSV(String csvContent) throws IOException, Cs } } + /** + * Prints the given CSV data in a formatted table + */ /** * Prints the given CSV data in a formatted table */ private static void printTable(List records) { if (records.isEmpty()) { - logger.info("No records found."); + System.out.println("No records found."); return; } @@ -727,7 +730,7 @@ private static void printTable(List records) { AnsiConsole.systemInstall(); // Print table header - logger.info(String.valueOf(ansi().fgYellow().a("=== CSV Data from S3 ===").reset())); + System.out.println(ansi().fgYellow().a("=== CSV Data from S3 ===").reset()); printRow(headers, columnWidths, true); // Print rows @@ -743,18 +746,18 @@ private static void printRow(String[] row, int[] columnWidths, boolean isHeader) .collect(Collectors.joining("+", "+", "+")); if (isHeader) { - logger.info(border); + System.out.println(border); } - logger.info("|"); + System.out.print("|"); for (int i = 0; i < columnWidths.length; i++) { String cell = (i < row.length && row[i] != null) ? row[i] : ""; - logger.info(" %-" + columnWidths[i] + "s |", isHeader ? ansi().fgBrightBlue().a(cell).reset() : cell); + System.out.printf(" %-" + columnWidths[i] + "s |", isHeader ? ansi().fgBrightBlue().a(cell).reset() : cell); } System.out.println(); if (isHeader) { - logger.info(border); + System.out.println(border); } } } From fc246ecacacb25d321385fd840191f025136e581 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 4 Mar 2025 16:29:09 -0500 Subject: [PATCH 143/144] updated the readme --- .../example_code/entityresolution/README.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md index d0a7195d8a5..26d4ccfefa5 100644 --- a/javav2/example_code/entityresolution/README.md +++ b/javav2/example_code/entityresolution/README.md @@ -45,16 +45,16 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. -- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L377) -- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L415) -- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L216) -- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L182) -- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L123) -- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L303) -- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L266) -- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) -- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L340) -- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L502) +- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L393) +- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L431) +- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L232) +- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L198) +- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L139) +- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L319) +- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L282) +- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L175) +- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L356) +- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L518) From 0287371afcb90652adaf5a835fd044756e488ca7 Mon Sep 17 00:00:00 2001 From: scmacdon Date: Tue, 4 Mar 2025 16:41:18 -0500 Subject: [PATCH 144/144] updated readme --- .../example_code/entityresolution/README.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/javav2/example_code/entityresolution/README.md b/javav2/example_code/entityresolution/README.md index d0a7195d8a5..26d4ccfefa5 100644 --- a/javav2/example_code/entityresolution/README.md +++ b/javav2/example_code/entityresolution/README.md @@ -45,16 +45,16 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. -- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L377) -- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L415) -- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L216) -- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L182) -- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L123) -- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L303) -- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L266) -- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L159) -- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L340) -- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L502) +- [CheckWorkflowStatus](src/main/java/com/example/entity/scenario/EntityResActions.java#L393) +- [CreateMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L431) +- [CreateSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L232) +- [DeleteMatchingWorkflow](src/main/java/com/example/entity/scenario/EntityResActions.java#L198) +- [DeleteSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L139) +- [GetMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L319) +- [GetSchemaMapping](src/main/java/com/example/entity/scenario/EntityResActions.java#L282) +- [ListSchemaMappings](src/main/java/com/example/entity/scenario/EntityResActions.java#L175) +- [StartMatchingJob](src/main/java/com/example/entity/scenario/EntityResActions.java#L356) +- [TagEntityResource](src/main/java/com/example/entity/scenario/EntityResActions.java#L518)