From afaff5fc51f5faf83bfc87772be839ee36788f5a Mon Sep 17 00:00:00 2001 From: reetkat Date: Wed, 29 Mar 2023 10:05:55 -0400 Subject: [PATCH] Upgrade to version 1.4.0 --- CHANGELOG.md | 16 +- README.md | 754 +++++++++++++----- .../create_batch_inference_job/handler.py | 5 + .../create_batch_segment_job/handler.py | 5 + source/aws_lambda/create_campaign/handler.py | 5 + source/aws_lambda/create_dataset/handler.py | 5 + .../create_dataset_group/handler.py | 5 + .../create_dataset_import_job/handler.py | 11 + .../create_event_tracker/handler.py | 5 + source/aws_lambda/create_filter/handler.py | 5 + .../aws_lambda/create_recommender/handler.py | 5 + source/aws_lambda/create_solution/handler.py | 10 +- .../create_solution_version/handler.py | 5 + .../shared/personalize/service_model.py | 2 +- .../aws_lambda/shared/personalize_service.py | 254 +++++- source/aws_lambda/shared/s3.py | 1 + source/aws_lambda/shared/sfn_middleware.py | 45 +- source/cdk_solution_helper_py/README.md | 88 +- .../cdk/aws_lambda/java/bundling.py | 1 - .../requirements/requirements.txt | 2 +- .../cdk/aws_lambda/python/bundling.py | 2 +- .../cdk/aws_lambda/python/function.py | 4 +- .../aws_solutions/cdk/synthesizers.py | 10 +- .../helpers_cdk/setup.py | 2 +- .../helpers_common/setup.py | 2 +- .../requirements-dev.txt | 6 +- source/images/solution-architecture.png | Bin 0 -> 275302 bytes source/infrastructure/cdk.json | 4 +- source/infrastructure/deploy.py | 3 +- .../functions/create_batch_inference_job.py | 2 + .../functions/create_batch_segment_job.py | 2 + .../aws_lambda/functions/create_campaign.py | 2 + .../aws_lambda/functions/create_dataset.py | 2 + .../functions/create_dataset_group.py | 2 + .../functions/create_dataset_import_job.py | 2 + .../functions/create_event_tracker.py | 3 + .../aws_lambda/functions/create_filter.py | 2 + .../functions/create_recommender.py | 2 + .../aws_lambda/functions/create_solution.py | 2 + .../functions/create_solution_version.py | 2 + .../requirements/requirements.txt | 2 +- .../batch_inference_jobs_fragment.py | 2 +- .../batch_segment_jobs_fragment.py | 2 +- .../step_functions/dataset_import_fragment.py | 22 +- .../step_functions/filter_fragment.py | 2 +- .../step_functions/solution_fragment.py | 38 +- source/requirements-dev.txt | 10 +- source/scheduler/README.md | 85 +- source/scheduler/cdk/setup.py | 2 +- source/scheduler/common/setup.py | 4 +- .../aspects/test_personalize_app_stack.py | 4 +- source/tests/aws_lambda/__init__.py | 12 + .../create_batch_inference_job/__init__.py | 12 + .../test_batch_inference_job_handler.py | 111 ++- .../create_batch_segment_job/__init__.py | 12 + .../test_batch_segment_job_handler.py | 112 ++- .../aws_lambda/create_campaign/__init__.py | 12 + .../test_create_campaign_handler.py | 125 ++- .../aws_lambda/create_config/__init__.py | 12 + .../test_create_config_handler.py | 12 +- .../aws_lambda/create_dataset/__init__.py | 12 + .../create_dataset/test_dataset_handler.py | 101 ++- .../create_dataset_group/__init__.py | 12 + .../test_dataset_group_handler.py | 139 +++- .../create_dataset_import_job/__init__.py | 12 + .../test_dataset_import_job_handler.py | 123 ++- .../create_event_tracker/__init__.py | 12 + .../test_create_event_tracker_handler.py | 97 ++- .../aws_lambda/create_filter/__init__.py | 12 + .../test_create_filter_handler.py | 96 ++- .../aws_lambda/create_recommender/__init__.py | 12 + .../test_create_recommender_handler.py | 93 ++- .../aws_lambda/create_schema/__init__.py | 12 + .../create_schema/create_schema_handler.py | 7 +- .../aws_lambda/create_solution/__init__.py | 12 + .../test_create_solution_handler.py | 96 ++- .../create_solution_version/__init__.py | 12 + .../test_create_solution_version_handler.py | 91 ++- .../s3_event/test_s3_event_handler.py | 35 +- .../sns_notification/test_sns_notification.py | 14 +- .../aws_lambda/test_personalize_service.py | 590 +++++++++++++- .../tests/aws_lambda/test_sfn_middleware.py | 35 +- .../aws_lambda/python/fixtures/pyproject.toml | 2 +- .../aws_lambda/python/test_function.py | 2 +- source/tests/conftest.py | 43 +- .../tests/fixtures/config/sample_config.json | 41 +- .../config/sample_config_root_tags.json | 149 ++++ .../fixtures/config/sample_config_wtags.json | 324 ++++++++ 88 files changed, 3511 insertions(+), 553 deletions(-) create mode 100644 source/images/solution-architecture.png create mode 100644 source/tests/aws_lambda/__init__.py create mode 100644 source/tests/aws_lambda/create_batch_inference_job/__init__.py create mode 100644 source/tests/aws_lambda/create_batch_segment_job/__init__.py create mode 100644 source/tests/aws_lambda/create_campaign/__init__.py create mode 100644 source/tests/aws_lambda/create_config/__init__.py create mode 100644 source/tests/aws_lambda/create_dataset/__init__.py create mode 100644 source/tests/aws_lambda/create_dataset_group/__init__.py create mode 100644 source/tests/aws_lambda/create_dataset_import_job/__init__.py create mode 100644 source/tests/aws_lambda/create_event_tracker/__init__.py create mode 100644 source/tests/aws_lambda/create_filter/__init__.py create mode 100644 source/tests/aws_lambda/create_recommender/__init__.py create mode 100644 source/tests/aws_lambda/create_schema/__init__.py create mode 100644 source/tests/aws_lambda/create_solution/__init__.py create mode 100644 source/tests/aws_lambda/create_solution_version/__init__.py create mode 100644 source/tests/fixtures/config/sample_config_root_tags.json create mode 100644 source/tests/fixtures/config/sample_config_wtags.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 662aff5..97ebad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2023-03-29 + +### Changed + +- Python library updates +- Upgraded Python runtime to 3.9 +- Using `performAutoML` field in creating solutions now logs error, but proceeds to build the solution. This field is deprecated by the service. + +### Added + +- Github [issue #16](https://github.com/aws-solutions/maintaining-personalized-experiences-with-machine-learning/issues/16) `tags` are supported for all component types, for example, dataset group, import jobs, solutions, etc. Root-level tags are also supported in the config. +- "UPDATE" model training is supported for input solutions trained with the User-Personalization recipe or the HRNN-Coldstart recipe. + ## [1.3.1] - 2022-12-19 -### Fixed +### Fixed - GitHub [issue #19](https://github.com/aws-solutions/maintaining-personalized-experiences-with-machine-learning/issues/19). This fix prevents AWS Service Catalog AppRegistry Application Name and Attribute Group Name from using a string that begins with `AWS`, since strings begining with `AWS` are considered as reserved words by the AWS Service. @@ -15,7 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Locked `boto3` to version `1.25.5`, and upgraded python library packages. - ## [1.3.0] - 2022-11-17 ### Added diff --git a/README.md b/README.md index 7685bca..154d485 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Maintaining Personalized Experiences with Machine Learning -The Maintaining Personalized Experiences with Machine Learning solution provides a mechanism to automate much of the -workflow around Amazon Personalize. This includes dataset group creation, dataset creation and import, solution + +The Maintaining Personalized Experiences with Machine Learning solution provides a mechanism to automate much of the +workflow around Amazon Personalize. This includes dataset group creation, dataset creation and import, solution creation, solution version creation, campaign creation and batch inference job creation Scheduled rules can be configured for setting up import jobs, solution version retraining (with campaign update) and @@ -14,11 +15,11 @@ batch inference job creation. - [Creating a custom build](#creating-a-custom-build) - [Collection of operational metrics](#collection-of-operational-metrics) -## Architecture +## Architecture The following describes the architecture of the solution -![architecture](source/images/solution-architecture.jpg) +![architecture](source/images/solution-architecture.png) The AWS CloudFormation template deploys the resources required to automate your Amazon Personalize usage and deployments. The template includes the following components: @@ -26,35 +27,34 @@ The template includes the following components: 1. An Amazon S3 bucket used to store personalization data and configuration files. 2. An AWS Lambda function triggered when new/ updated personalization configuration is uploaded to the personalization data bucket. 3. An AWS Stepfunctions workflow to manage all of the resources of an Amazon Personalize dataset group (including datasets, schemas, event tracker, filters, solutions, campaigns, and batch inference jobs). -4. CloudWatch metrics for Amazon Personalize for each new trained solution version are added to help you evaluate the performance of a model over time. -5. An Amazon Simple Notification Service (SNS) topic and subscription to notify an administrator when the maintenance workflow has completed via email. -6. DynamoDB is used to track the scheduled events configured for Amazon Personalize to fully or partially retrain solutions, (re) import datasets and perform batch inference jobs. +4. Amazon CloudWatch metrics for Amazon Personalize for each new trained solution version are added to help you evaluate the performance of a model over time. +5. An Amazon Simple Notification Service (SNS) topic and subscription to notify an administrator when the maintenance workflow has completed via email. +6. Amazon DynamoDB is used to track the scheduled events configured for Amazon Personalize to fully or partially retrain solutions, (re) import datasets and perform batch inference jobs. 7. An AWS Stepfunctions workflow is used to track the current running scheduled events, and invokes step functions to perform solution maintenance (creating new solution versions, updating campaigns), import updated datasets, and perform batch inference. 8. A set of maintenance AWS Stepfunctions workflows are provided to: - 1. Create new dataset import jobs on schedule - 2. Perform solution FULL retraining on schedule (and update associated campaigns) - 3. Perform solution UPDATE retraining on schedule (and update associated campaigns) - 4. Create batch inference jobs + 1. Create new dataset import jobs on schedule + 2. Perform solution FULL retraining on schedule (and update associated campaigns) + 3. Perform solution UPDATE retraining on schedule (and update associated campaigns) + 4. Create batch inference jobs 9. An Amazon EventBridge event bus, where resource status notification updates are posted throughout the AWS Step -functions workflow + functions workflow 10. A command line interface (CLI) lets existing resources be imported and allows schedules to be established for -resources that already exist in Amazon Personalize - + resources that already exist in Amazon Personalize -**Note**: From v1.0.0, AWS CloudFormation template resources are created by the [AWS CDK](https://aws.amazon.com/cdk/) -and [AWS Solutions Constructs](https://aws.amazon.com/solutions/constructs/). +> **Note**: From v1.0.0, AWS CloudFormation template resources are created by the [AWS CDK](https://aws.amazon.com/cdk/) +> and [AWS Solutions Constructs](https://aws.amazon.com/solutions/constructs/). -### AWS CDK Constructs +### AWS CDK Constructs [AWS CDK Solutions Constructs](https://aws.amazon.com/solutions/constructs/) make it easier to consistently create -well-architected applications. All AWS Solutions Constructs are reviewed by AWS and use best practices established by -the AWS Well-Architected Framework. This solution uses the following AWS CDK Solutions Constructs: +well-architected applications. All AWS Solutions Constructs are reviewed by AWS and use best practices established by +the AWS Well-Architected Framework. This solution uses the following AWS CDK Solutions Constructs: - [aws-lambda-sns](https://docs.aws.amazon.com/solutions/latest/constructs/aws-lambda-sns.html) ## Deployment -You can launch this solution with one click from [AWS Solutions Implementations](https://aws.amazon.com/solutions/implementations/maintaining-personalized-experiences-with-ml). +You can launch this solution with one click from [AWS Solutions Implementations](https://aws.amazon.com/solutions/implementations/maintaining-personalized-experiences-with-ml). To customize the solution, or to contribute to the solution, see [Creating a custom build](#creating-a-custom-build) @@ -63,7 +63,8 @@ To customize the solution, or to contribute to the solution, see [Creating a cus This solution uses **parameter files**. The parameter file contains all the necessary information to create and maintain your resources in Amazon Personalize. -The file can contain the following sections +The file can contain the following sections + - `datasetGroup` - `datasets` - `solutions` (can contain `campaigns` and `batchInferenceJobs`) @@ -73,168 +74,168 @@ The file can contain the following sections
See a sample of the parameter file -```json +```json { - "datasetGroup": { - "serviceConfig": { - "name": "dataset-group-name" - }, - "workflowConfig": { - "schedules": { - "import": "cron(0 */6 * * ? *)" - } - } - }, - "datasets": { - "users": { - "dataset": { - "serviceConfig": { - "name": "users-data" - } - }, - "schema": { - "serviceConfig": { - "name": "users-schema", - "schema": { - "type": "record", - "name": "users", - "namespace": "com.amazonaws.personalize.schema", - "fields": [ - { - "name": "USER_ID", - "type": "string" - }, - { - "name": "AGE", - "type": "int" - }, - { - "name": "GENDER", - "type": "string", - "categorical": true - } - ] - } - } - } - }, - "interactions": { - "dataset": { - "serviceConfig": { - "name": "interactions-data" - } - }, - "schema": { - "serviceConfig": { - "name": "interactions-schema", - "schema": { - "type": "record", - "name": "interactions", - "namespace": "com.amazonaws.personalize.schema", - "fields": [ - { - "name": "ITEM_ID", - "type": "string" - }, - { - "name": "USER_ID", - "type": "string" - }, - { - "name": "TIMESTAMP", - "type": "long" - }, - { - "name": "EVENT_TYPE", - "type": "string" - }, - { - "name": "EVENT_VALUE", - "type": "float" - } - ] - } - } - } - } - }, - "solutions": [ - { - "serviceConfig": { - "name": "sims-solution", - "recipeArn": "arn:aws:personalize:::recipe/aws-sims" - }, - "workflowConfig": { - "schedules": { - "full": "cron(0 0 ? * 1 *)" - } - } - }, - { - "serviceConfig": { - "name": "popularity-count-solution", - "recipeArn": "arn:aws:personalize:::recipe/aws-popularity-count" - }, - "workflowConfig": { - "schedules": { - "full": "cron(0 1 ? * 1 *)" - } - } - }, - { - "serviceConfig": { - "name": "user-personalization-solution", - "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization" - }, - "workflowConfig": { - "schedules": { - "full": "cron(0 2 ? * 1 *)" - } - }, - "campaigns": [ - { - "serviceConfig": { - "name": "user-personalization-campaign", - "minProvisionedTPS": 1 - } - } - ], - "batchInferenceJobs": [ - { - "serviceConfig": {}, - "workflowConfig": { - "schedule": "cron(0 3 * * ? *)" - } - } - ] - } - ], - "eventTracker": { - "serviceConfig": { - "name": "dataset-group-name-event-tracker" - } - }, - "filters": [ - { - "serviceConfig": { - "name": "clicked-or-streamed", - "filterExpression": "INCLUDE ItemID WHERE Interactions.EVENT_TYPE in (\"click\", \"stream\")" - } - }, - { - "serviceConfig": { - "name": "interacted", - "filterExpression": "INCLUDE ItemID WHERE Interactions.EVENT_TYPE in (\"*\")" - } - } - ] + "datasetGroup": { + "serviceConfig": { + "name": "dataset-group-name-1" + }, + "workflowConfig": { + "schedules": { + "import": "cron(0 */6 * * ? *)" + } + } + }, + "datasets": { + "users": { + "dataset": { + "serviceConfig": { + "name": "users-data" + } + }, + "schema": { + "serviceConfig": { + "name": "users-schema", + "schema": { + "type": "record", + "name": "users", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "AGE", + "type": "int" + }, + { + "name": "GENDER", + "type": "string", + "categorical": true + } + ] + } + } + } + }, + "interactions": { + "dataset": { + "serviceConfig": { + "name": "interactions-data" + } + }, + "schema": { + "serviceConfig": { + "name": "interactions-schema", + "schema": { + "type": "record", + "name": "interactions", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "TIMESTAMP", + "type": "long" + }, + { + "name": "EVENT_TYPE", + "type": "string" + }, + { + "name": "EVENT_VALUE", + "type": "float" + } + ] + } + } + } + } + }, + "solutions": [ + { + "serviceConfig": { + "name": "sims-solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-sims" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 0 ? * 1 *)" + } + } + }, + { + "serviceConfig": { + "name": "popularity-count-solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-popularity-count" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 1 ? * 1 *)" + } + } + }, + { + "serviceConfig": { + "name": "user-personalization-solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 2 ? * 1 *)" + } + }, + "campaigns": [ + { + "serviceConfig": { + "name": "user-personalization-campaign", + "minProvisionedTPS": 1 + } + } + ], + "batchInferenceJobs": [ + { + "serviceConfig": {}, + "workflowConfig": { + "schedule": "cron(0 3 * * ? *)" + } + } + ] + } + ], + "eventTracker": { + "serviceConfig": { + "name": "dataset-group-name-event-tracker" + } + }, + "filters": [ + { + "serviceConfig": { + "name": "clicked-or-streamed", + "filterExpression": "INCLUDE ItemID WHERE Interactions.EVENT_TYPE in (\"click\", \"stream\")" + } + }, + { + "serviceConfig": { + "name": "interacted", + "filterExpression": "INCLUDE ItemID WHERE Interactions.EVENT_TYPE in (\"*\")" + } + } + ] } ```
-This solution allows you to manage multiple dataset groups through the use of multiple parameter files. All .json files -discovered under the `train/` prefix will trigger the workflow however, the following structure is recommended: +This solution allows you to manage multiple dataset groups through the use of multiple parameter files. All .json files +discovered under the `train/` prefix will trigger the workflow however, the following structure is recommended: ``` train/ @@ -244,7 +245,7 @@ train/ │ ├── interactions.csv │ ├── items.csv (optional) │ └── users.csv (optional) -│ +│ └── / (option 2 - multiple csv files for data import) ├── config.json ├── interactions/ @@ -261,7 +262,7 @@ train/ └── .csv ``` -If batch inference jobs are required, [batch inference job configuration files](https://docs.aws.amazon.com/personalize/latest/dg/recommendations-batch.html#batch-data-upload) +If batch inference jobs are required, [batch inference job configuration files](https://docs.aws.amazon.com/personalize/latest/dg/recommendations-batch.html#batch-data-upload) must also be uploaded to the following lcoation: ``` @@ -269,7 +270,7 @@ batch/ │ └── / └── / - └── job_config.json + └── job_config.json ``` Batch inference output will be produced at the following location: @@ -281,74 +282,395 @@ batch/ └── / └── / ├── _CHECK - └── job_config.json.out + └── job_config.json.out +``` + +Note: It is not recommended to use `performAutoML` as this feature will be deprecated in the future. Please take the time to select the most appropriate recipe for your use-case. If this parameter is used for this solution in the configuration, it will log an error and continue to build the solution without it. Please refer [FAQs](https://github.com/aws-samples/amazon-personalize-samples/blob/master/PersonalizeCheatSheet2.0.md) and [AWS Personalize Developer Guide](https://docs.aws.amazon.com/personalize/latest/dg/API_CreateSolution.html#personalize-CreateSolution-request-performAutoML). + +## Configuration with Tags + +You can also optionally supply tags in your configurations: + +```json +{ + "datasetGroup": { + "serviceConfig": { + "name": "dataset-group-name-2", + "tags": [ + { + "tagKey": "dataset-group-key", + "tagValue": "dataset-group-value" + } + ] + } + }, + "datasets": { + "serviceConfig": { + "importMode": "FULL", + "tags": [ + { + "tagKey": "datasets-key", + "tagValue": "datasets-value" + } + ] + }, + "interactions": { + "dataset": { + "serviceConfig": { + "name": "interactions-data", + "tags": [ + { + "tagKey": "interactions-dataset-key", + "tagValue": "interactions-dataset-value" + } + ] + } + }, + "schema": { + "serviceConfig": { + "name": "interactions-schema", + "schema": { + "type": "record", + "name": "Interactions", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "TIMESTAMP", + "type": "long" + }, + { + "name": "EVENT_TYPE", + "type": "string" + } + ], + "version": "1.0" + } + } + } + }, + "items": { + "dataset": { + "serviceConfig": { + "name": "items-data", + "tags": [ + { + "tagKey": "items-dataset-key", + "tagValue": "items-dataset-value" + } + ] + } + }, + "schema": { + "serviceConfig": { + "name": "items-schema", + "schema": { + "type": "record", + "name": "Items", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "GENRES", + "type": "string", + "categorical": true + }, + { + "name": "YEAR", + "type": "int" + }, + { + "name": "CREATION_TIMESTAMP", + "type": "long" + } + ], + "version": "1.0" + } + } + } + }, + "users": { + "dataset": { + "serviceConfig": { + "name": "users-data", + "tags": [ + { + "tagKey": "users-dataset-key", + "tagValue": "users-dataset-value" + } + ] + } + }, + "schema": { + "serviceConfig": { + "name": "users-schema", + "schema": { + "type": "record", + "name": "Users", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "GENDER", + "type": "string", + "categorical": true + } + ], + "version": "1.0" + } + } + } + } + }, + "eventTracker": { + "serviceConfig": { + "name": "event-tracker-name", + "tags": [ + { + "tagKey": "event-tracker-key", + "tagValue": "event-tracker-value" + } + ] + } + }, + "solutions": [ + { + "serviceConfig": { + "name": "solution-recommender-user-personalization", + "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization", + "performHPO": true, + "tags": [ + { + "tagKey": "solution-key", + "tagValue": "solution-value" + } + ], + "solutionVersion": { + "name": "solutionV1", + "trainingMode": "FULL", + "tags": [ + { + "tagKey": "solution-version-key", + "tagValue": "solution-version-value" + } + ] + } + } + } + ] +} ``` -Note: It is not recommended to use `performAutoML` as this feature will be deprecated in the future. Please take the time to select the most appropriate recipe for the use-case and skip this feature. Refer [FAQs](https://github.com/aws-samples/amazon-personalize-samples/blob/master/PersonalizeCheatSheet2.0.md). +Note: You cannot tag already created resources through the configuration. Only "FULL" `importMode` for datasets is currently supported. -## Creating a custom build -To customize the solution, follow the steps below: +Tags can also be root-level tags and they apply to all components which do not have tags specified. For example, for the datasetGroup `dataset-group-name-3` specified below, `tagKey` "project" and `tagValue` "user-personalization" applies to `datasetGroup`, `interactions` dataset and its import, the `eventTracker`, and the `solutionVersion`, but the dataset-import gets the specified values for tagKey and tagValue as "datasets-key" and "datasets-value" respectively (applies for dataset imports of users, interactions and items datasets) and solution `solution-user-personalization` gets "solution-key" and "solution-value": + +```json +{ + "tags": [ + { + "tagKey": "project", + "tagValue": "user-personalization" + } + ], + "datasetGroup": { + "serviceConfig": { + "name": "dataset-group-name-3" + } + }, + "datasets": { + "serviceConfig": { + "importMode": "FULL", + "tags": [ + { + "tagKey": "datasets-key", + "tagValue": "datasets-value" + } + ] + }, + "interactions": { + "dataset": { + "serviceConfig": { + "name": "interactions-data" + } + }, + "schema": { + "serviceConfig": { + "name": "interactions-schema", + "schema": { + "type": "record", + "name": "Interactions", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "TIMESTAMP", + "type": "long" + }, + { + "name": "EVENT_TYPE", + "type": "string" + } + ], + "version": "1.0" + } + } + } + } + }, + "eventTracker": { + "serviceConfig": { + "name": "event-tracker-name" + } + }, + "solutions": [ + { + "serviceConfig": { + "name": "solution-user-personalization", + "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization", + "performHPO": true, + "tags": [ + { + "tagKey": "solution-key", + "tagValue": "solution-value" + } + ] + } + } + ] +} +``` + +Some key points: + +1. Solution version tags can be specified inside the solution's `serviceConfig` field, inside the `solutionVersion` field (see the `dataset-group-name-2` example). `solutionVersion` specification is optional. +2. Root-level tags apply to all components which do not have explicit tags specified (see notes around `dataset-group-name-2` json example). +3. [`tags`](https://docs.aws.amazon.com/personalize/latest/dg/tagging-resources.html) are optional fields. + +## Training Mode + +Training mode can be described as 'FULL' or 'UPDATE' through the `solutionVersion` field inside the `solution` specification. + +The purpose of trainingMode="UPDATE" is to process new items added to the items dataset (via PutItems or a bulk upload) as well as impression data for new interactions added to the interactions since the last FULL/UPDATE training. The UPDATE mode only brings in new items and impression data and does not retrain the model. Therefore, if there are no dataset updates since the last FULL/UPDATE training, you might get an error saying "There should be updates to at least one dataset after last active solution version with training mode set to FULL". + +With User-Personalization, Amazon Personalize automatically updates the latest model (solution version) every two hours behind the scenes to include new data. There is no cost for automatic updates. The solution version must be deployed with an [Amazon Personalize campaign](https://docs.aws.amazon.com/personalize/latest/dg/campaigns.html) for updates to occur. Your campaign automatically uses the updated solution version. No new solution version is created when an auto update completes and no new model metrics are generated. This is because no full retraining occurs. If you create a new solution version, Amazon Personalize will not automatically update older solution versions, even if you have deployed them in a campaign. Updates also do not occur if you have deleted your dataset. + +If every two hours is not frequent enough, you can manually create a solution version with trainingMode set to UPDATE to include those new items in recommendations. Amazon Personalize automatically updates only your latest fully trained solution version, so the manually updated solution version won't be automatically updated in the future. Also note that if you create a solution version with UPDATE, you will be charged for the server hours to perform the update. + +For more information about automatic updates, see the [Amazon Personalize Developer Guide](https://docs.aws.amazon.com/personalize/latest/dg/native-recipe-new-item-USER_PERSONALIZATION.html#automatic-updates) + +```json +... +"solutions": [ + { + "serviceConfig": { + "name": "affinity_item", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity", + "solutionVersion": { + "trainingMode": "UPDATE" + "tags": [{"tagKey": "project", "tagValue": "item-affinity"}] + } + }, + ... + } +] +... + +``` + +## Creating a custom build + +To customize the solution, follow the steps below: ### Prerequisites + The following procedures assumes that all the OS-level configuration has been completed. They are: -* [AWS Command Line Interface](https://aws.amazon.com/cli/) -* [Python](https://www.python.org/) 3.9 or newer -* [Node.js](https://nodejs.org/en/) 16.x or newer -* [AWS CDK](https://aws.amazon.com/cdk/) 2.44.0 or newer -* [Amazon Corretto OpenJDK](https://docs.aws.amazon.com/corretto/) 17.0.4.1 +- [AWS Command Line Interface](https://aws.amazon.com/cli/) +- [Python](https://www.python.org/) 3.9 or newer +- [Node.js](https://nodejs.org/en/) 16.x or newer +- [AWS CDK](https://aws.amazon.com/cdk/) 2.44.0 or newer +- [Amazon Corretto OpenJDK](https://docs.aws.amazon.com/corretto/) 17.0.4.1 > **Please ensure you test the templates before updating any production deployments.** ### 1. Download or clone this repo + ``` + git clone https://github.com/aws-solutions/maintaining-personalized-experiences-with-machine-learning + ``` -### 2. Create a Python virtual environment for development -```bash -python -m virtualenv .venv -source ./.venv/bin/activate -cd ./source -pip install -r requirements-dev.txt +### 2. Create a Python virtual environment for development + +```bash +python -m virtualenv .venv +source ./.venv/bin/activate +cd ./source +pip install -r requirements-dev.txt ``` ### 2. After introducing changes, run the unit tests to make sure the customizations don't break existing functionality + ```bash -pytest --cov +pytest --cov ``` ### 3. Build the solution for deployment -#### Using AWS CDK (recommended) +#### Using AWS CDK (recommended) + Packaging and deploying the solution with the AWS CDK allows for the most flexibility in development -```bash -cd ./source/infrastructure + +```bash +cd ./source/infrastructure # set environment variables required by the solution export BUCKET_NAME="my-bucket-name" -# bootstrap CDK (required once - deploys a CDK bootstrap CloudFormation stack for assets) +# bootstrap CDK (required once - deploys a CDK bootstrap CloudFormation stack for assets) cdk bootstrap --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess -# build the solution +# build the solution cdk synth -# build and deploy the solution +# build and deploy the solution cdk deploy ``` -#### Using the solution build tools +#### Using the solution build tools + It is highly recommended to use the AWS CDK to deploy this solution (using the instructions above). While CDK is used to develop the solution, to package the solution for release as a CloudFormation template, use the `build-s3-cdk-dist` -build tool: +build tool: ```bash cd ./deployment -export DIST_BUCKET_PREFIX=my-bucket-name -export SOLUTION_NAME=my-solution-name -export VERSION=my-version +export DIST_BUCKET_PREFIX=my-bucket-name +export SOLUTION_NAME=my-solution-name +export VERSION=my-version export REGION_NAME=my-region build-s3-cdk-dist deploy \ @@ -362,30 +684,32 @@ build-s3-cdk-dist deploy \ ``` **Parameter Details** -- `$DIST_BUCKET_PREFIX` - The S3 bucket name prefix. A randomized value is recommended. You will need to create an + +- `$DIST_BUCKET_PREFIX` - The S3 bucket name prefix. A randomized value is recommended. You will need to create an S3 bucket where the name is `-`. The solution's CloudFormation template will expect the source code to be located in the bucket matching that name. - `$SOLUTION_NAME` - The name of This solution (example: personalize-solution-customization) - `$VERSION` - The version number to use (example: v0.0.1) - `$REGION_NAME` - The region name to use (example: us-east-1) -This will result in all global assets being pushed to the `DIST_BUCKET_PREFIX`, and all regional assets being pushed to +This will result in all global assets being pushed to the `DIST_BUCKET_PREFIX`, and all regional assets being pushed to `DIST_BUCKET_PREFIX-`. If your `REGION_NAME` is us-east-1, and the `DIST_BUCKET_PREFIX` is -`my-bucket-name`, ensure that both `my-bucket-name` and `my-bucket-name-us-east-1` exist and are owned by you. +`my-bucket-name`, ensure that both `my-bucket-name` and `my-bucket-name-us-east-1` exist and are owned by you. After running the command, you can deploy the template: -* Get the link of the `SOLUTION_NAME.template` uploaded to your Amazon S3 bucket -* Deploy the solution to your account by launching a new AWS CloudFormation stack using the link of the template above. +- Get the link of the `SOLUTION_NAME.template` uploaded to your Amazon S3 bucket +- Deploy the solution to your account by launching a new AWS CloudFormation stack using the link of the template above. > **Note:** `build-s3-cdk-dist` will use your current configured `AWS_REGION` and `AWS_PROFILE`. To set your defaults, > install the [AWS Command Line Interface](https://aws.amazon.com/cli/) and run `aws configure`. ## Collection of operational metrics + This solution collects anonymous operational metrics to help AWS improve the quality of features of the solution. For more information, including how to disable this capability, please see the [implementation guide](https://docs.aws.amazon.com/solutions/latest/maintaining-personalized-experiences-with-ml/collection-of-operational-metrics.html). - -*** + +--- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -399,4 +723,4 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/source/aws_lambda/create_batch_inference_job/handler.py b/source/aws_lambda/create_batch_inference_job/handler.py index 08cca79..b16eec7 100644 --- a/source/aws_lambda/create_batch_inference_job/handler.py +++ b/source/aws_lambda/create_batch_inference_job/handler.py @@ -62,6 +62,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_batch_segment_job/handler.py b/source/aws_lambda/create_batch_segment_job/handler.py index e055405..f2c30df 100644 --- a/source/aws_lambda/create_batch_segment_job/handler.py +++ b/source/aws_lambda/create_batch_segment_job/handler.py @@ -57,6 +57,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_campaign/handler.py b/source/aws_lambda/create_campaign/handler.py index 06109ab..3a5becd 100644 --- a/source/aws_lambda/create_campaign/handler.py +++ b/source/aws_lambda/create_campaign/handler.py @@ -51,6 +51,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_dataset/handler.py b/source/aws_lambda/create_dataset/handler.py index 7c74d04..1f0f0b6 100644 --- a/source/aws_lambda/create_dataset/handler.py +++ b/source/aws_lambda/create_dataset/handler.py @@ -39,6 +39,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_dataset_group/handler.py b/source/aws_lambda/create_dataset_group/handler.py index 20fcc21..a76fafc 100644 --- a/source/aws_lambda/create_dataset_group/handler.py +++ b/source/aws_lambda/create_dataset_group/handler.py @@ -46,6 +46,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } tracer = Tracer() diff --git a/source/aws_lambda/create_dataset_import_job/handler.py b/source/aws_lambda/create_dataset_import_job/handler.py index fc08b24..fa08fd1 100644 --- a/source/aws_lambda/create_dataset_import_job/handler.py +++ b/source/aws_lambda/create_dataset_import_job/handler.py @@ -46,6 +46,17 @@ "default": "omit", "as": "iso8601", }, + "importMode": {"source": "event", "path": "serviceConfig.importMode", "default": "omit"}, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, + "publishAttributionMetricsToS3": { + "source": "event", + "path": "serviceConfig.publishAttributionMetricsToS3", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_event_tracker/handler.py b/source/aws_lambda/create_event_tracker/handler.py index 9a87406..76f23e3 100644 --- a/source/aws_lambda/create_event_tracker/handler.py +++ b/source/aws_lambda/create_event_tracker/handler.py @@ -35,6 +35,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_filter/handler.py b/source/aws_lambda/create_filter/handler.py index 473d098..9a56a49 100644 --- a/source/aws_lambda/create_filter/handler.py +++ b/source/aws_lambda/create_filter/handler.py @@ -39,6 +39,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_recommender/handler.py b/source/aws_lambda/create_recommender/handler.py index 3e02880..b61127f 100644 --- a/source/aws_lambda/create_recommender/handler.py +++ b/source/aws_lambda/create_recommender/handler.py @@ -41,6 +41,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() diff --git a/source/aws_lambda/create_solution/handler.py b/source/aws_lambda/create_solution/handler.py index a458227..d9816bd 100644 --- a/source/aws_lambda/create_solution/handler.py +++ b/source/aws_lambda/create_solution/handler.py @@ -30,11 +30,6 @@ "path": "serviceConfig.performHPO", "default": "omit", }, - "performAutoML": { - "source": "event", - "path": "serviceConfig.performAutoML", - "default": "omit", - }, "recipeArn": { "source": "event", "path": "serviceConfig.recipeArn", @@ -60,6 +55,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() tracer = Tracer() diff --git a/source/aws_lambda/create_solution_version/handler.py b/source/aws_lambda/create_solution_version/handler.py index 594c5c8..3388389 100644 --- a/source/aws_lambda/create_solution_version/handler.py +++ b/source/aws_lambda/create_solution_version/handler.py @@ -47,6 +47,11 @@ "default": "omit", "as": "iso8601", }, + "tags": { + "source": "event", + "path": "serviceConfig.tags", + "default": "omit", + }, } logger = Logger() tracer = Tracer() diff --git a/source/aws_lambda/shared/personalize/service_model.py b/source/aws_lambda/shared/personalize/service_model.py index 93bbf02..66cd0b4 100644 --- a/source/aws_lambda/shared/personalize/service_model.py +++ b/source/aws_lambda/shared/personalize/service_model.py @@ -133,7 +133,7 @@ def _filter(self, result: Dict) -> Dict: result.pop("accountId", None) result.pop("trackingId", None) - # datset + # dataset result.pop("datasetType", None) # schema diff --git a/source/aws_lambda/shared/personalize_service.py b/source/aws_lambda/shared/personalize_service.py index a76e14d..bd96b9f 100644 --- a/source/aws_lambda/shared/personalize_service.py +++ b/source/aws_lambda/shared/personalize_service.py @@ -12,45 +12,45 @@ # ###################################################################################################################### import json import re +import time from datetime import datetime from pathlib import Path -from typing import Callable, Dict, Optional, List, Union +from typing import Callable, Dict, List, Optional, Union import avro.schema import botocore.exceptions import jmespath from aws_lambda_powertools import Logger, Metrics from aws_lambda_powertools.metrics import MetricUnit, SchemaValidationError -from botocore.stub import Stubber -from dateutil.tz import tzlocal - from aws_solutions.core import ( - get_service_client, + get_aws_account, get_aws_partition, get_aws_region, - get_aws_account, + get_service_client, ) -from aws_solutions.scheduler.common import ScheduleError, Schedule +from aws_solutions.scheduler.common import Schedule, ScheduleError +from botocore.stub import Stubber +from dateutil.tz import tzlocal from shared.events import Notifies from shared.exceptions import ( - ResourcePending, - ResourceNeedsUpdate, ResourceFailed, + ResourceNeedsUpdate, + ResourcePending, SolutionVersionPending, ) from shared.resource import ( - Resource, + BatchInferenceJob, + BatchSegmentJob, + Campaign, Dataset, - EventTracker, DatasetGroup, DatasetImportJob, + EventTracker, + Filter, + Resource, + Schema, Solution, SolutionVersion, - BatchInferenceJob, - BatchSegmentJob, - Schema, - Filter, - Campaign, ) from shared.s3 import S3 @@ -122,6 +122,12 @@ def describe(self, resource: Resource, **kwargs): else: return self.describe_default(resource, **kwargs) + def list_tags_for_resource(self, **kwargs): + logger.debug(f"listing tags for {kwargs}") + describe_fn_name = f"list_tags_for_resource" + describe_fn = getattr(self.cli, describe_fn_name) + return describe_fn(**kwargs) + def describe_default(self, resource: Resource, **kwargs): """ Describe a resource in Amazon Personalize by deriving its ARN from its name @@ -164,6 +170,10 @@ def describe_with_update(self, resource: Resource, **kwargs): kwargs = self._remove_workflow_parameters(resource, kwargs.copy()) result = self.describe_default(resource, **kwargs) for k, v in kwargs.items(): + # tags are not returned in any describe call + if k == "tags": + continue + received = result[resource.name.camel][k] expected = v @@ -487,11 +497,12 @@ class InputValidator: @classmethod def validate(cls, method: str, expected_params: Dict) -> None: """ - Validate an Amazon Personalize resource using the botocore stubber + Validate an Amazon Personalize resource config parameters using the botocore stubber :return: None. Raises ParamValidationError if the InputValidator fails to validate """ cli = get_service_client("personalize") func = getattr(cli, method) + with Stubber(cli) as stubber: stubber.add_response(method, {}, expected_params) func(**expected_params) @@ -499,6 +510,7 @@ def validate(cls, method: str, expected_params: Dict) -> None: class Configuration: _schema = [ + {"tags": []}, { "datasetGroup": [ "serviceConfig", @@ -512,6 +524,7 @@ class Configuration: }, { "datasets": [ + "serviceConfig", { "users": [ {"dataset": ["serviceConfig"]}, @@ -580,35 +593,66 @@ def __init__(self): self._configuration_errors = [] self.config_dict = {} self.dataset_group = "UNKNOWN" + self.pass_root_tags = False - def load(self, content: Union[Path, str]): - if isinstance(content, Path): - config_str = content.read_text(encoding="utf-8") + def load(self, content: Union[Path, str, dict]): + if isinstance(content, dict): + self.config_dict = content else: - config_str = content + if isinstance(content, Path): + config_str = content.read_text(encoding="utf-8") + else: + config_str = content + self.config_dict = self._decode(config_str) - self.config_dict = self._decode(config_str) + self.pass_root_tags = jmespath.search("tags", self.config_dict) def validate(self): self._validate_not_empty() self._validate_keys() + self._validate_root_tags() + self._validate_tags( + "datasetGroup.serviceConfig.tags", + "datasets.serviceConfig.tags", + "datasets.interactions.dataset.serviceConfig.tags", + "datasets.users.dataset.serviceConfig.tags", + "datasets.items.dataset.serviceConfig.tags", + "filters[].serviceConfig.tags", + "eventTracker.serviceConfig.tags", + "solutions[].serviceConfig.tags", + "solutions[].serviceConfig.solutionVersion.tags", + "solutions[].campaigns[].serviceConfig.tags", + "recommenders[].serviceConfig.tags", + "solutions[].batchInferenceJobs[].serviceConfig.tags", + "solutions[].batchSegmentJobs[].serviceConfig.tags", + ) self._validate_dataset_group() self._validate_schemas() self._validate_datasets() + self._validate_dataset_import_job() self._validate_event_tracker() self._validate_filters() self._validate_solutions() self._validate_solution_update() + self._validate_recommender() self._validate_cron_expressions( "datasetGroup.workflowConfig.schedules.import", "solutions[].workflowConfig.schedules.full", "solutions[].workflowConfig.schedules.update", "solutions[].batchInferenceJobs[].workflowConfig.schedule", ) + self._validate_naming() return len(self._configuration_errors) == 0 + def config_dict_wdefaults(self): + self._validate_not_empty() + self._validate_dataset_import_job() + self._validate_solutions() + self._validate_solution_update() + return self.config_dict + @property def errors(self) -> List[str]: return self._configuration_errors @@ -630,6 +674,7 @@ def _validate_resource(self, resource: Resource, expected_params): try: InputValidator.validate(f"create_{resource.name.snake}", expected_params) + except botocore.exceptions.ParamValidationError as exc: self._configuration_errors.append(str(exc).replace("\n", " ")) @@ -641,6 +686,7 @@ def _validate_dataset_group(self, path="datasetGroup.serviceConfig"): self._validate_resource(DatasetGroup(), dataset_group) if isinstance(dataset_group, dict): self.dataset_group = dataset_group.get("name", self.dataset_group) + self._fill_default_vals("datasetGroup", dataset_group) def _validate_event_tracker(self, path="eventTracker.serviceConfig"): event_tracker = jmespath.search(path, self.config_dict) @@ -653,7 +699,9 @@ def _validate_event_tracker(self, path="eventTracker.serviceConfig"): return event_tracker["datasetGroupArn"] = DatasetGroup().arn("validation") + self._validate_resource(EventTracker(), event_tracker) + self._fill_default_vals("eventTracker", event_tracker) def _validate_filters(self, path="filters[].serviceConfig"): filters = jmespath.search(path, self.config_dict) or {} @@ -663,6 +711,7 @@ def _validate_filters(self, path="filters[].serviceConfig"): _filter["datasetGroupArn"] = DatasetGroup().arn("validation") self._validate_resource(Filter(), _filter) + self._fill_default_vals("filter", _filter) def _validate_type(self, var, typ, err: str): validates = isinstance(var, typ) @@ -702,11 +751,59 @@ def _validate_solutions(self, path="solutions[]"): ) _solution = _solution.get("serviceConfig") + if not self._validate_type(_solution, dict, f"solutions[{idx}].serviceConfig must be an object"): continue + # `performAutoML` is currently returned from InputValidator.validate() as a valid field + # Once the botocore Stubber is updated to not have this param anymore in `create_solution` call, + # this check can be deleted. + if "performAutoML" in _solution: + del _solution["performAutoML"] + logger.error( + "performAutoML is not a valid configuration parameter - proceeding to create the " + "solution without this feature. For more details, refer to the Maintaining Personalized Experiences " + "Github project's README.md file." + ) + _solution["datasetGroupArn"] = DatasetGroup().arn("validation") - self._validate_resource(Solution(), _solution) + if "solutionVersion" in _solution: + # To pass solution through InputValidator + solution_version_config = _solution["solutionVersion"] + del _solution["solutionVersion"] + self._validate_resource(Solution(), _solution) + _solution["solutionVersion"] = solution_version_config + + else: + self._validate_resource(Solution(), _solution) + + self._fill_default_vals("solution", _solution) + self._validate_solution_version(_solution) + + def _validate_solution_version(self, solution_config): + allowed_sol_version_keys = ["trainingMode", "tags"] + + if "solutionVersion" not in solution_config: + solution_config["solutionVersion"] = {} + else: + keys_not_allowed = set(solution_config["solutionVersion"].keys()) - set(allowed_sol_version_keys) + if keys_not_allowed != set(): + self._configuration_errors.append( + f"Allowed keys for solutionVersion are: {allowed_sol_version_keys}. Unsupported key(s): {list(keys_not_allowed)}" + ) + + self._fill_default_vals("solutionVersion", solution_config["solutionVersion"]) + + def _validate_recommender(self, path="recommenders[]"): + recommenders = jmespath.search(path, self.config_dict) or {} + for idx, recommender_config in enumerate(recommenders): + if not self._validate_type( + recommender_config, dict, f"recommenders[{idx}].serviceConfig must be an object" + ): + continue + + _recommender = recommender_config.get("serviceConfig") + self._fill_default_vals("recommender", _recommender) def _validate_solution_update(self): invalid = ( @@ -721,21 +818,6 @@ def _validate_solution_update(self): f"solution {solution_name} does not support solution version incremental updates - please use `full` instead of `update`." ) - def _validate_solution_versions(self, path: str, solution_versions: List[Dict]): - for idx, solution_version_config in enumerate(solution_versions): - current_path = f"{path}.solutionVersions[{idx}]" - - solution_version = solution_version_config.get("solutionVersion") - if not self._validate_type( - solution_version, - dict, - f"{current_path}.solutionVersion must be an object", - ): - continue - else: - solution_version["solutionArn"] = Solution().arn("validation") - self._validate_resource(SolutionVersion(), solution_version) - def _validate_campaigns(self, path, campaigns: List[Dict]): for idx, campaign_config in enumerate(campaigns): current_path = f"{path}.campaigns[{idx}]" @@ -747,6 +829,8 @@ def _validate_campaigns(self, path, campaigns: List[Dict]): campaign["solutionVersionArn"] = SolutionVersion().arn("validation") self._validate_resource(Campaign(), campaign) + self._fill_default_vals("campaign", campaign) + def _validate_batch_inference_jobs(self, path, solution_name, batch_inference_jobs: List[Dict]): for idx, batch_job_config in enumerate(batch_inference_jobs): current_path = f"{path}.batchInferenceJobs[{idx}]" @@ -773,6 +857,7 @@ def _validate_batch_inference_jobs(self, path, solution_name, batch_inference_jo } ) self._validate_resource(BatchInferenceJob(), batch_job) + self._fill_default_vals("batchJob", batch_job) def _validate_batch_segment_jobs(self, path, solution_name, batch_segment_jobs: List[Dict]): for idx, batch_job_config in enumerate(batch_segment_jobs): @@ -800,6 +885,7 @@ def _validate_batch_segment_jobs(self, path, solution_name, batch_segment_jobs: } ) self._validate_resource(BatchSegmentJob(), batch_job) + self._fill_default_vals("segmentJob", batch_job) def _validate_rate(self, expression): rate_re = re.compile(r"rate\((?P\d+) (?P(minutes?|hours?|day?s)\))") @@ -867,7 +953,6 @@ def _validate_datasets(self) -> None: return # some values are provided by the solution - we introduce placeholders - SolutionVersion().arn("validation") dataset.update( { "datasetGroupArn": DatasetGroup().arn("validation"), @@ -876,6 +961,20 @@ def _validate_datasets(self) -> None: } ) self._validate_resource(Dataset(), dataset) + self._fill_default_vals("dataset", dataset) + + def _validate_dataset_import_job(self, path="datasets.serviceConfig") -> None: + """ + Perform a validation of the dataset import fields to ensure default values are present + :return: None + """ + dataset_import = jmespath.search(path, self.config_dict) + if "datasets" in self.config_dict: + if not dataset_import: + self.config_dict["datasets"]["serviceConfig"] = {} + dataset_import = jmespath.search(path, self.config_dict) + + self._fill_default_vals("datasetImport", dataset_import) def _validate_schemas(self) -> None: """ @@ -938,6 +1037,43 @@ def _validate_keys(self, config: Dict = None, schema: List = None, path=""): else: self._configuration_errors.append(f"an unknown validation error occurred at {path}") + def _validate_tag_types(self, result, path): + err_msg = f"Invalid type at path {path} for tags, expected list[dict]." + is_lst = self._validate_type(result, list, err_msg) + if isinstance(result, list) and isinstance(result[0], list): # sometimes jmespath returns list of list instead + result = result[0] + + if is_lst: + for tag_instance in result: + is_dict = self._validate_type(tag_instance, dict, err_msg) + if path == "root": + if is_dict and set(tag_instance.keys()) == {"tagKey", "tagValue"}: + continue + else: + self._configuration_errors.append( + "Parameter validation failed: Tag keys must be one of: 'tagKey', 'tagValue'" + ) + return False + else: + return is_dict + return False + + def _validate_root_tags(self): + if "tags" in self.config_dict: + self._validate_tag_types(self.config_dict["tags"], "root") + + def _validate_tags(self, *paths: List[str]): + """ + Validate the configuration in config_dict for all tags provided. + Ensures that the tags supplied are a list of dict, and only contain the allowed key values. + :param paths: The paths in config_dict (used in recursion to identify a jmespath path) that may contain tags + :return: None + """ + for path in paths: + result = jmespath.search(path, self.config_dict) + if result: + self._validate_tag_types(result, path) + def _validate_list(self, config: List, schema: List, path=""): for idx, item in enumerate(config): current_path = f"{path}[{idx}]" @@ -971,3 +1107,43 @@ def _validate_naming(self): """Validate that names of resources don't overlap in ways that might cause issues""" self._validate_no_duplicates(name="campaign names", path="solutions[].campaigns[].serviceConfig.name") self._validate_no_duplicates(name="solution names", path="solutions[].serviceConfig.name") + + def _fill_default_vals(self, resource_type, resource_dict): + """Insert default values for tags and other fields whenever not supplied""" + + if ( + resource_type + in [ + "datasetGroup", + "datasetImport", + "dataset", + "eventTracker", + "solution", + "solutionVersion", + "filter", + "recommender", + "campaign", + "batchJob", + "segmentJob", + ] + and "tags" not in resource_dict + ): + if self.pass_root_tags: + resource_dict["tags"] = self.config_dict["tags"] + else: + resource_dict["tags"] = [] + + if resource_type == "datasetImport": + if "importMode" not in resource_dict: + resource_dict["importMode"] = "FULL" + if "publishAttributionMetricsToS3" not in resource_dict: + resource_dict["publishAttributionMetricsToS3"] = False + + if resource_type == "solutionVersion": + if "tags" not in resource_dict: + if self.pass_root_tags: + resource_dict["tags"] = self.config_dict["tags"] + else: + resource_dict["tags"] = [] + if "trainingMode" not in resource_dict: + resource_dict["trainingMode"] = "FULL" diff --git a/source/aws_lambda/shared/s3.py b/source/aws_lambda/shared/s3.py index bbbc1b1..44ece2a 100644 --- a/source/aws_lambda/shared/s3.py +++ b/source/aws_lambda/shared/s3.py @@ -60,6 +60,7 @@ def _exists_one(self): return True def _exists_any(self): + latest = None try: bucket = self.cli.Bucket(self.bucket) objects = [ diff --git a/source/aws_lambda/shared/sfn_middleware.py b/source/aws_lambda/shared/sfn_middleware.py index 95c7955..245430a 100644 --- a/source/aws_lambda/shared/sfn_middleware.py +++ b/source/aws_lambda/shared/sfn_middleware.py @@ -19,22 +19,21 @@ from dataclasses import dataclass, field from enum import Enum, auto from pathlib import Path -from typing import Dict, Any, Callable, Optional, List, Union +from typing import Any, Callable, Dict, List, Optional, Union from uuid import uuid4 import jmespath from aws_lambda_powertools import Logger -from dateutil.parser import isoparse - from aws_solutions.core import get_service_client +from dateutil.parser import isoparse from shared.date_helpers import parse_datetime from shared.exceptions import ( - ResourcePending, - ResourceInvalid, ResourceFailed, + ResourceInvalid, ResourceNeedsUpdate, + ResourcePending, ) -from shared.personalize_service import Personalize +from shared.personalize_service import Configuration, Personalize from shared.resource import get_resource logger = Logger() @@ -48,10 +47,7 @@ STATUS_FAILED = "CREATE FAILED" STATUS_ACTIVE = "ACTIVE" -WORKFLOW_PARAMETERS = { - "maxAge", - "timeStarted", -} +WORKFLOW_PARAMETERS = {"maxAge", "timeStarted"} WORKFLOW_CONFIG_DEFAULT = {"timeStarted": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")} @@ -86,13 +82,15 @@ def set_workflow_config(config: Dict) -> Dict: "batchSegmentJobs": Arity.MANY, "filters": Arity.MANY, "solutionVersion": Arity.ONE, + "tags": Arity.MANY, } # Note: schema creation notification is not supported at this time # Note: dataset, dataset import job, event tracker notifications are added in the workflow - for k, v in config.items(): - if k in {"serviceConfig", "workflowConfig", "bucket", "currentDate"}: - pass # do not modify any serviceConfig keys + for k in config: + v = config[k] + if k in {"serviceConfig", "workflowConfig", "bucket", "currentDate", "tags"}: + pass # do not modify any serviceConfig keys/tags elif k in resources.keys() and resources[k] == Arity.ONE: config[k].setdefault("workflowConfig", {}) config[k]["workflowConfig"] |= WORKFLOW_CONFIG_DEFAULT @@ -104,6 +102,10 @@ def set_workflow_config(config: Dict) -> Dict: else: config[k] = set_workflow_config(config[k]) if config[k] else config[k] + cfg = Configuration() + cfg.load(config) + config = cfg.config_dict_wdefaults() + return config @@ -264,11 +266,16 @@ def check_status(self, resource: Dict[str, Any], **expected) -> Dict: # NOSONAR actual_value = actual_value.lower() expected_value = expected_value.lower() + if expected_key == "tags": + continue + # some parameters don't require checking: if self.resource == "datasetImportJob" and expected_key in { "jobName", "dataSource", "roleArn", + "importMode", + "publishAttributionMetricsToS3", }: continue if self.resource.startswith("batch") and expected_key in { @@ -278,8 +285,16 @@ def check_status(self, resource: Dict[str, Any], **expected) -> Dict: # NOSONAR "roleArn", }: continue - if self.resource == "solutionVersion" and expected_key == "trainingMode": - continue + + if self.resource == "solutionVersion": + if expected_key == "trainingMode": + continue + if expected_key == "name": + if "/" in actual_value: # user provided name. + actual_value = actual_value.split("/")[-1] + if "solution_" in actual_value: # name was auto-generated as default value. + continue + if expected_key in WORKFLOW_PARAMETERS: continue diff --git a/source/cdk_solution_helper_py/README.md b/source/cdk_solution_helper_py/README.md index 4c0762a..4f6abcd 100644 --- a/source/cdk_solution_helper_py/README.md +++ b/source/cdk_solution_helper_py/README.md @@ -1,45 +1,46 @@ # CDK Solution Helper for Python and CDK + ## Infrastructure Deployment Tooling -This tooling helps you develop new AWS Solutions using the AWS CDK with an approach that is compatible with the -current AWS Solutions build pipeline. - -This README summarizes using the tool. +This tooling helps you develop new AWS Solutions using the AWS CDK with an approach that is compatible with the +current AWS Solutions build pipeline. + +This README summarizes using the tool. ## Prerequisites Install this package. It requires at least -- Python 3.7 -- AWS CDK version 2.7.0 or higher +- Python 3.9 +- AWS CDK version 2.44.0 or higher -To install the packages: +To install the packages: ``` pip install /cdk_solution_helper_py/helpers_cdk # where is the path to the solution helper -pip install /cdk_solution_helper_py/helpers_common # where is the path to the solution helper +pip install /cdk_solution_helper_py/helpers_common # where is the path to the solution helper ``` - + ## 1. Create a new CDK application ```shell script -mkdir -p your_solution_name/deployment +mkdir -p your_solution_name/deployment mkdir -p your_solution_name/source-infrastructure cd your_solution_name/source/infrastructure cdk init app --language=python . ``` -## 2. Install the package +## 2. Install the package ``` cd your_solution_name -virtualenv .venv +virtualenv .venv source ./.venv/bin/activate pip install /cdk_solution_helper_py/helpers_cdk # where is the path to the solution helper pip install /cdk_solution_helper_py/helpers_common # where is the path to the solution helper ``` -# 3. Write CDK code using the helpers +# 3. Write CDK code using the helpers This might be a file called `app.py` in your CDK application directory @@ -77,7 +78,7 @@ logger = logging.getLogger("cdk-helper") solution = CDKSolution(cdk_json_path=Path(__file__).parent.absolute() / "cdk.json") -# Inherit from SolutionStack to create a CDK app compatible with AWS Solutions +# Inherit from SolutionStack to create a CDK app compatible with AWS Solutions class MyStack(SolutionStack): def __init__(self, scope: Construct, construct_id: str, description: str, template_filename, **kwargs): super().__init__(scope, construct_id, description, template_filename, **kwargs) @@ -100,8 +101,8 @@ class MyStack(SolutionStack): # add any custom metrics to your stack! self.metrics.update({"your_custom_metric": "your_custom_metric_value"}) - - # example of adding an AWS Lambda function for Python + + # example of adding an AWS Lambda function for Python SolutionsPythonFunction( self, "ExampleLambdaFunction", @@ -138,15 +139,14 @@ if __name__ == "__main__": result = build_app() ``` - ## 4. Build the solution for deployment You can use the [AWS CDK](https://aws.amazon.com/cdk/) to deploy the solution directly ```shell script -# install the Python dependencies -cd -virtualenv .venv +# install the Python dependencies +cd +virtualenv .venv source .venv/bin/activate pip install -r source/requirements-build-and-test.txt @@ -156,22 +156,22 @@ cd source/infrastructure # set environment variables required by the solution - use your own bucket name here export BUCKET_NAME="placeholder" -# bootstrap CDK (required once - deploys a CDK bootstrap CloudFormation stack for assets) +# bootstrap CDK (required once - deploys a CDK bootstrap CloudFormation stack for assets) cdk bootstrap --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess # deploy with CDK cdk deploy -# +# ``` At this point, the stack will be built and deployed using CDK - the template will take on default CloudFormation parameter values. To modify the stack parameters, you can use the `--parameters` flag in CDK deploy - for example: ```shell script -cdk deploy --parameters [...] +cdk deploy --parameters [...] ``` -## 5. Package the solution for release +## 5. Package the solution for release It is highly recommended to use CDK to deploy this solution (see step #1 above). While CDK is used to develop the solution, to package the solution for release as a CloudFormation template use the `build-s3-cdk-dist` script: @@ -183,55 +183,55 @@ export DIST_OUTPUT_BUCKET=my-bucket-name export SOLUTION_NAME=my-solution-name export VERSION=my-version -build-s3-cdk-dist deploy --source-bucket-name $DIST_OUTPUT_BUCKET --solution-name $SOLUTION_NAME --version-code $VERSION --cdk-app-path ../source/infrastructure/app.py --cdk-app-entrypoint app:build_app --sync +build-s3-cdk-dist deploy --source-bucket-name $DIST_OUTPUT_BUCKET --solution-name $SOLUTION_NAME --version-code $VERSION --cdk-app-path ../source/infrastructure/app.py --cdk-app-entrypoint app:build_app --sync ``` > **Note**: `build-s3-cdk-dist` will use your current configured `AWS_REGION` and `AWS_PROFILE`. To set your defaults -install the [AWS Command Line Interface](https://aws.amazon.com/cli/) and run `aws configure`. +> install the [AWS Command Line Interface](https://aws.amazon.com/cli/) and run `aws configure`. #### Parameter Details: - + - `$DIST_OUTPUT_BUCKET` - This is the global name of the distribution. For the bucket name, the AWS Region is added to -the global name (example: 'my-bucket-name-us-east-1') to create a regional bucket. The lambda artifact should be -uploaded to the regional buckets for the CloudFormation template to pick it up for deployment. + the global name (example: 'my-bucket-name-us-east-1') to create a regional bucket. The lambda artifact should be + uploaded to the regional buckets for the CloudFormation template to pick it up for deployment. - `$SOLUTION_NAME` - The name of This solution (example: your-solution-name) - `$VERSION` - The version number of the change -> **Notes**: The `build_s3_cdk_dist` script expects the bucket name as one of its parameters, and this value should -not include the region suffix. See below on how to create the buckets expected by this solution: -> -> The `SOLUTION_NAME`, and `VERSION` variables might also be defined in the `cdk.json` file. +> **Notes**: The `build_s3_cdk_dist` script expects the bucket name as one of its parameters, and this value should +> not include the region suffix. See below on how to create the buckets expected by this solution: +> +> The `SOLUTION_NAME`, and `VERSION` variables might also be defined in the `cdk.json` file. ## 3. Upload deployment assets to yur Amazon S3 buckets Create the CloudFormation bucket defined above, as well as a regional bucket in the region you wish to deploy. The CloudFormation template is configured to pull the Lambda deployment packages from Amazon S3 bucket in the region the template is being launched in. Create a bucket in the desired region with the region name appended to the name of the -bucket. eg: for us-east-1 create a bucket named: ```my-bucket-us-east-1```. +bucket. eg: for us-east-1 create a bucket named: `my-bucket-us-east-1`. For example: -```bash +```bash aws s3 mb s3://my-bucket-name --region us-east-1 aws s3 mb s3://my-bucket-name-us-east-1 --region us-east-1 ``` -Copy the built S3 assets to your S3 buckets: +Copy the built S3 assets to your S3 buckets: ``` use the --sync option of build-s3-cdk-dist to upload the global and regional assets ``` -> **Notes**: Choose your desired region by changing region in the above example from us-east-1 to your desired region -of the S3 buckets. +> **Notes**: Choose your desired region by changing region in the above example from us-east-1 to your desired region +> of the S3 buckets. ## 4. Launch the CloudFormation template -* Get the link of `your-solution-name.template` uploaded to your Amazon S3 bucket. -* Deploy the solution to your account by launching a new AWS CloudFormation stack using the link of the -`your-solution-name.template`. - -*** +- Get the link of `your-solution-name.template` uploaded to your Amazon S3 bucket. +- Deploy the solution to your account by launching a new AWS CloudFormation stack using the link of the + `your-solution-name.template`. + +--- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -245,4 +245,4 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py index f613a09..242074a 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py @@ -90,7 +90,6 @@ def _invoke_local_command( cwd: Union[str, Path, None] = None, return_stdout: bool = False, ): - cwd = Path(cwd) rv = "" diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/layers/aws_lambda_powertools/requirements/requirements.txt b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/layers/aws_lambda_powertools/requirements/requirements.txt index c3edbd4..1bb43d3 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/layers/aws_lambda_powertools/requirements/requirements.txt +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/layers/aws_lambda_powertools/requirements/requirements.txt @@ -1,2 +1,2 @@ -aws-lambda-powertools==1.29.2 +aws-lambda-powertools==2.10.0 aws-xray-sdk==2.11.0 \ No newline at end of file diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py index e3e9276..c86985c 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py @@ -25,7 +25,7 @@ from aws_solutions.cdk.helpers import copytree -DEFAULT_RUNTIME = Runtime.PYTHON_3_7 +DEFAULT_RUNTIME = Runtime.PYTHON_3_9 BUNDLER_DEPENDENCIES_CACHE = "/var/dependencies" REQUIREMENTS_TXT_FILE = "requirements.txt" REQUIREMENTS_PIPENV_FILE = "Pipfile" diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/function.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/function.py index ed497fa..7c66898 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/function.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/function.py @@ -27,7 +27,7 @@ from aws_solutions.cdk.aws_lambda.python.bundling import SolutionsPythonBundling from aws_solutions.cdk.aws_lambda.python.hash_utils import DirectoryHash -DEFAULT_RUNTIME = Runtime.PYTHON_3_7 +DEFAULT_RUNTIME = Runtime.PYTHON_3_9 DEPENDENCY_EXCLUDES = ["*.pyc"] @@ -62,7 +62,7 @@ def __init__( if not kwargs.get("role"): kwargs["role"] = self._create_role() - # python 3.7 is selected to support custom resources and inline code + # python 3.9 is selected to support custom resources and inline code if not kwargs.get("runtime"): kwargs["runtime"] = DEFAULT_RUNTIME diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py index 630419b..019cec7 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py @@ -68,7 +68,7 @@ def delete_bootstrap_parameters(self): def delete_cdk_helpers(self): """Remove the CDK bucket deployment helpers, since solutions don't have a bootstrap bucket.""" to_delete = [] - for (resource_name, resource) in self.contents.get("Resources", {}).items(): + for resource_name, resource in self.contents.get("Resources", {}).items(): if "Custom::CDKBucketDeployment" in resource["Type"]: to_delete.append(resource_name) if "CDKBucketDeployment" in resource_name: @@ -89,7 +89,7 @@ def patch_nested(self): ] }, ) - for (resource_name, resource) in self.contents.get("Resources", {}).items(): + for resource_name, resource in self.contents.get("Resources", {}).items(): resource_type = resource.get("Type") if resource_type == "AWS::CloudFormation::Stack": try: @@ -120,7 +120,7 @@ def patch_nested(self): def patch_lambda(self): """Patch the lambda functions for S3 deployment compatibility""" - for (resource_name, resource) in self.contents.get("Resources", {}).items(): + for resource_name, resource in self.contents.get("Resources", {}).items(): resource_type = resource.get("Type") if resource_type == "AWS::Lambda::Function" or resource_type == "AWS::Lambda::LayerVersion": logger.info(f"{resource_name} ({resource_type}) patching") @@ -190,7 +190,7 @@ def patch_lambda(self): def patch_app_reg(self): """Patch the App Registry Info""" - for (resource_name, resource) in self.contents.get("Resources", {}).items(): + for resource_name, resource in self.contents.get("Resources", {}).items(): resource_type = resource.get("Type") if resource_type == "AWS::ApplicationInsights::Application": logger.info(f"{resource_name} ({resource_type}) patching") @@ -248,7 +248,7 @@ def save(self, asset_path_global: Path = None, asset_path_regional: Path = None) str(asset_path.joinpath(self.global_asset_name)), ) - # regional solutions assets - default folder location is "regional-s3-assets" + # the regional solutions assets - default folder location is "regional-s3-assets" if asset_path_regional: asset_path = self._build_asset_path(asset_path_regional) for asset in self.assets_regional: diff --git a/source/cdk_solution_helper_py/helpers_cdk/setup.py b/source/cdk_solution_helper_py/helpers_cdk/setup.py index 04cc93f..8211189 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/setup.py +++ b/source/cdk_solution_helper_py/helpers_cdk/setup.py @@ -52,7 +52,7 @@ def get_version(): "pip>=22.3.1", "aws_cdk_lib==2.44.0", "Click==8.1.3", - "boto3==1.25.5", + "boto3==1.26.47", "requests==2.28.1", "crhelper==2.0.11", ], diff --git a/source/cdk_solution_helper_py/helpers_common/setup.py b/source/cdk_solution_helper_py/helpers_common/setup.py index 2266334..a89ffad 100644 --- a/source/cdk_solution_helper_py/helpers_common/setup.py +++ b/source/cdk_solution_helper_py/helpers_common/setup.py @@ -42,7 +42,7 @@ def get_version(): license="Apache License 2.0", packages=setuptools.find_namespace_packages(exclude=["build*"]), install_requires=[ - "boto3==1.25.5", + "boto3==1.26.47", "pip>=22.3.1", ], python_requires=">=3.9", diff --git a/source/cdk_solution_helper_py/requirements-dev.txt b/source/cdk_solution_helper_py/requirements-dev.txt index 10fdc74..38f633e 100644 --- a/source/cdk_solution_helper_py/requirements-dev.txt +++ b/source/cdk_solution_helper_py/requirements-dev.txt @@ -1,16 +1,16 @@ aws_cdk_lib==2.44.0 aws-cdk.aws-servicecatalogappregistry-alpha==2.44.0a0 black -boto3==1.25.5 +boto3==1.26.47 requests==2.28.1 crhelper==2.0.11 Click moto pipenv poetry -pytest +pytest>=7.2.0 pytest-cov>=4.0.0 -pytest-mock>=3.9.0 +pytest-mock>=3.10.0 tox tox-pyenv -e ./source/cdk_solution_helper_py/helpers_cdk diff --git a/source/images/solution-architecture.png b/source/images/solution-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..ae0fe77569e76a6a95183f1d762dfa5b2fae4d32 GIT binary patch literal 275302 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%0A`#AY`8berkPdb+JJ(5|CE3@h{u_BA4{ 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ⅆXIRSd zsmmFmc)@Q)S463$cI)rCivpTz(C8f)7RmTGR`;R2ln+e0yc-D7O2GT<{TNAb27ay! zDxcoopnK%kG=P#(?{8kbta|K5n#PseFdK6ljhH67zloW(qdg=lyX1n)%l+o+qW`t-!MxrXp^!VTq{PM+6&u#>ozD+i z-tX``)zGsMe^9Tm6x1)LV&>y*PybTdr1pyDc`H(~E6wA#dH0nefV~y>Gj3GmUawr~ zw0Guf7tj2s88J4lyjf6XYU4#HRke z;fKBWvazduYdBKjozp?CrzAa zK`k-+Alwhkno+A!y>1?94uul_Ona1X2?XN`6Z1QbMd1glWr+D$q9feI90x=}0L2kZ zr;<{%(|S>|^YtPNJ^y*$J@5Fudww260LKfP4smlYfC;8vF*#vrlW=k}9_es$eFFk` zvp^hzpzk?r`HxrpzIe#reLbrn=_ZQ^6`Q{LjLu!b2!vbz*{9hV?&o1> za{+j^?uvMSFGV;dU=QZ3P$hu0s+fYY!aPpPs>eHvVNeEL4q)wBz)VI6+HGErQ_{>! zu1mtpq`WDj1|9}6btgt~ahOkeYlzD*%HT==45QR$7=pFlPZrHm)d!&$7bB!EhWzbY zRn_w@L5{G!HMNDzrv0d%2b=P>ugmkVuhJ|EHk}ZPJSr-B0|BJ{vZKh)cE%jRBn)Mr zCL0dO2Ur@wR~44xEJyRZ6@j%7G&UyHtUK3U9PPI9^ME|YW?T#YPO;j}RZTiX+I2Ty zPhqcngn(%i#$NSz^ULJH_gt5}euqVvcY&E{wFn@lNW+5%89^8>wMMz@D-5lKM#v#r zxG{(UJ6#|xpx*{79k59GNW&!XlT}uQrc?@=D7qdAWPp2avdEHU2u zxNMsL@bMK=gS{SQ<)gc52L>DR0_MqZDCdDt3~QhkOxY4=&R7PTt(g$E9TF(cV27y0 z!D|K>B@tGXK6=I!N85;c^6h|m^v$A}j9n8q6DlY@wg&dVcK7l>ZAmzE_y6~^rc(AX zqQ8kjrU-Z}0=`PNJ7j?T4LmhWhtiaZj&7%MJ~$ni`~U^euz|tu6fX3M&QP&T<&M!xkbwttg4MD{!2gm#0kYYb44^0&`X8Ja*JqkiBgsXSLFSDP zBy@iU`05$3`hpo$pkq2e-*x+<+=br$?&t-1m&h;M;37RLgE$)fL3wkMx-~NsD(rW- z;_oyW)QL~t($-N);xg%}>rty$Wv$g6rEVUaZ=PBtEKYm$H8^evmF~K_T3YlSrEDG? zou$ldemZOPYFf+!Vm?&w=BW0%=QcFp9b5~NV?*z)h^5n-Nj-$doAjTUu#z~KKkh>* zeBcnE6qp4Fr@{-XdVVOBESo$K*9N4{=(29M={C;*Y= z&_|8ZaNR;s##X&giAM}W&w2iuc=Qec%NmGf0j4OqUDSY-PSn2DFlbjSlTW1NSsYl# zqy2luQgHAPOi?)a6O=N)l)@;e^0;@Ay2Ry2;13Ep2vnH{$>Y^j1t5IbX}*E+5sZid z4b(hx+89s5eMk}6Od5+)BLM5j)@-Rlh$@9HY8? z3Ghb?m8d7oO2u8ClxFhI)5m;+B!P8a1#gyzs&2lKnEXz{irqm4ib&e4cA$GHx?RWLp+8)W+!&v7~9%ow3Exl{$j2f zj|^W&e^30+c|-i9d}baQ3Q2*(kY?GJy!se{1$=w+EPUe>AdY$nVD((KlNE3?Z@yAu z5+eX`YCJ$E$^ph1@l3Ib)@`uMpU~iiEqQ))jrL&^)-4kd)oy3pu1n%FMeBck{$kII zU(vY4fXnpYyY%(hltE!vEf=Z`=?%_Kx&GJ(5qv@@*jybJ9Q+dSgvbH@EdVcP^XLrgq5tGNRyXO(`gRL@5x_Zws)SEnU#`MrOV8E~SlExK}h&C1Km|;8I|K zZ9My7&)QHLd8r5UBi&Q>t1pA|rN9AK-JDd~84HLU>|%Y_9|HGn@qg&R6s*WPC0ghA*oFR;g#PDR^6CVZVivB#c^CzUUWDQCUmQ>ZUU9D|yXbcIrqf04D^itr^V z{DyS>Oz`v~yx9Yyc={G3n=ZWUYtg2po~=NNdbz@jLGoZ9@+PQykq06!@AHEp>%;JN zM7g`5z`Bqh1~KUc6#hyZfcFy}K!)A1pWw9DF>~gx-q^<*G3oByoQcv&>=W>?Gkw_z z%&jK4{^*N{T8UXm5m~>oN7Sr;XLE$sMgU9~UqA9t0?3$%CL_+(ilOlExk|19qVaB{ zkV5*8^&j}?sWGV#1Z60PqIo1xz-ziinVMd|#_vZ#UcA|a2 zigKWE%wc_Mh%q)=7G?=%Kp8C^SJht0YSqre6JR@Y?Aky_BBwL9GFlf_`&$_vI@r>B%iD75kno`kE;} z+iH}@*tlcm>RjLulz}Cfi%H2zzWT@~9zoI76$#%w*8n`*{B!OHs4f+FHWGEub;?O}E%d%kgLt&ksi!6^%40FK#S| zxfXKu{!rULLM~x>Z^(ZPD`^WZMC1AZ=r3j5%Ih(ZqTy3Zb_5`i!48=6*UvX&5JLN9 z*ZUYRi9{!|_=ph|YN{J*o6B=M|)+!F5!j=_3YS2gZYyEI^#4FvN+vvP5utHKkREqcebKNKs z5Ou}+M?E0)er~uLE>{-{NRD#i))sX(Ygm=L->0~UEOqFip3jeu{mr=Ks(XzqRmJ!|6-uG0_$qOw#1sPaL9x)5%+1qz9Zh~1YXPcfpm#i z0EEb>n&y)QkP3taN~E?dv+X}Vc20?nDk5Jh362R$!$?sC1SVe2M~e@`>v1jhSxH-v z*gx}!$*_wU+Xdm{Cirmbx#MHGUgK*b8+6zP_WPgdD70}~Lc^%o@= z1rRN~$r5HITk08UB|ey|bJ!uDRr8O`1csMRbpu zFhovOgQ&SIJ~7<5g&qSsk4yl}f#&_A{y33~Qw>1eN(Wq*_gj$;1b0Atg4^!rdyAjm zaK8dTx71CYLTy&PM$cMwzjnMs0IUk6QAsg0c3+`A`uT~refab!uCe!WDS%}By*vxB1W&n;)_u%-3*#?I`5Kxw$3{E z#nmTsWn{&TDfC>kBw05xch(h)d?EDMIP*WQwTHx|Carh^yTXyr=~Yrz^XFAgQ>2taeP=x_v~~rqu#7 zGQU;t&4oOSY5xA9mJo#;k~Hlo*Yl7H^`j7;&H`-41y+?n@{x3Lj9VA=SU=n_&b2tp`E1+%^4k5aP-5;=|V&NV?1Ft=p?*Q1xYzzdOSz!`Xy*# zl2`^|4Mr}9y@rxY5FQmZbv9@KbkPKKJt16xB8^DMy<%D%^t=!c0TNk0hxvfKSSI0< zHSV*s)aS=?Aedw^sD2U)+$04sy&BesQSOHVev%LK7aPxyZ9uz!h2YuV@DSjR>W*Vg ztUuYP;{t5D4XqvT`-Ka2YVy>xLz9HP^lzdPQh@14uR&+a>iV6LFG$6*bH)x->9g&i z8{~U&_4Lrg{Np-*9P4c4N~*vQsmQMZUB8-U!)UZF$tW^jK_qC;h_9Y`ZV;b23A&X9 z^~y|ri;U{DVZoWsfvSjO&s@NdkBnY&0uu#oYlO4@((nBYHzS&h@CcL;=|@(vnIu(f z4JUQ0wZ7V{O?n%lpScYmI<_5d+DvOQkJY*>M0w`#G2sV>Q%JRVGSYTUm9PqH!e!p? z9~O>Z{~5^7tH$kL8t+JY<+EVEyObn-KO9Pdb}PcGk4=?<8^b43XR;oEG;)rQAfMfm=#RQF4fFa6*Ku-pvA1ltmS!r^kZi} zjD2;$hhMWf12i?NgL%7vN4p#rrUW6iwzWM3q`zEdov25%{ut(n}zH0fL@D& zRzBt>zw_!cW;+;Cg=3q^Bw@Rs{x~KYIg^3?;o6fr>)mvTAal?&_(4gT7u;*UbaJbO z3vW^L?J@34hfX>rOB8u|HlTbpfL&%ilu7WVv1XYVDHBV%MK4}ML?@-00-84&lVG}b zHLn|;e4OW^!P)1L(){vCNDC6TpJCXwKU4)AxPc85{h+H)4q1SIijkV`w`ARGg{EkHzrlT)5GkJ7y{bK z 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 BQ`7(e literal 0 HcmV?d00001 diff --git a/source/infrastructure/cdk.json b/source/infrastructure/cdk.json index 3ad87bd..ce6dfbc 100644 --- a/source/infrastructure/cdk.json +++ b/source/infrastructure/cdk.json @@ -3,8 +3,8 @@ "context": { "SOLUTION_NAME": "Maintaining Personalized Experiences with Machine Learning", "SOLUTION_ID": "SO0170", - "SOLUTION_VERSION": "v1.3.1", + "SOLUTION_VERSION": "v1.4.0", "APP_REGISTRY_NAME": "personalized-experiences-ML", "APPLICATION_TYPE": "AWS-Solutions" } -} +} \ No newline at end of file diff --git a/source/infrastructure/deploy.py b/source/infrastructure/deploy.py index 5645787..6f1f4d4 100644 --- a/source/infrastructure/deploy.py +++ b/source/infrastructure/deploy.py @@ -17,10 +17,9 @@ from pathlib import Path import aws_cdk as cdk - +from aspects.app_registry import AppRegistry from aws_solutions.cdk import CDKSolution from personalize.stack import PersonalizeStack -from aspects.app_registry import AppRegistry logger = logging.getLogger("cdk-helper") solution = CDKSolution(cdk_json_path=Path(__file__).parent.absolute() / "cdk.json") diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py b/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py index 09cfea6..ef876b2 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py @@ -84,6 +84,8 @@ def _set_permissions(self): "personalize:DescribeBatchInferenceJob", "personalize:DescribeSolution", "personalize:DescribeSolutionVersion", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py b/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py index 4cb4510..99b62fc 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py @@ -84,6 +84,8 @@ def _set_permissions(self): "personalize:DescribeBatchSegmentJob", "personalize:DescribeSolution", "personalize:DescribeSolutionVersion", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_campaign.py b/source/infrastructure/personalize/aws_lambda/functions/create_campaign.py index 67aba1c..7542aeb 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_campaign.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_campaign.py @@ -44,6 +44,8 @@ def _set_permissions(self): "personalize:ListCampaigns", "personalize:DescribeCampaign", "personalize:UpdateCampaign", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_dataset.py b/source/infrastructure/personalize/aws_lambda/functions/create_dataset.py index 3013068..a45b9d5 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_dataset.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_dataset.py @@ -47,6 +47,8 @@ def _set_permissions(self): "personalize:CreateDataset", "personalize:DescribeDataset", "personalize:ListDatasets", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_dataset_group.py b/source/infrastructure/personalize/aws_lambda/functions/create_dataset_group.py index af4483e..117349f 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_dataset_group.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_dataset_group.py @@ -128,6 +128,8 @@ def _set_permissions(self): actions=[ "personalize:DescribeDatasetGroup", "personalize:CreateDatasetGroup", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[f"arn:{Aws.PARTITION}:personalize:{Aws.REGION}:{Aws.ACCOUNT_ID}:dataset-group/*"], diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py b/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py index 9668670..c1803a9 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py @@ -92,6 +92,8 @@ def _set_permissions(self): "personalize:CreateDatasetImportJob", "personalize:DescribeDatasetImportJob", "personalize:ListDatasetImportJobs", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_event_tracker.py b/source/infrastructure/personalize/aws_lambda/functions/create_event_tracker.py index 48b5bea..5e87aa3 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_event_tracker.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_event_tracker.py @@ -36,6 +36,7 @@ def __init__( { "serviceConfig": { "name.$": "$.eventTracker.serviceConfig.name", + "tags.$": "$.eventTracker.serviceConfig.tags", "datasetGroupArn.$": "$.datasetGroup.serviceConfig.datasetGroupArn", }, "workflowConfig": { @@ -57,6 +58,8 @@ def _set_permissions(self): "personalize:DescribeEventTracker", "personalize:ListEventTrackers", "personalize:CreateEventTracker", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_filter.py b/source/infrastructure/personalize/aws_lambda/functions/create_filter.py index d540fcc..cc436a9 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_filter.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_filter.py @@ -45,6 +45,8 @@ def _set_permissions(self): "personalize:DescribeDatasetGroup", "personalize:CreateFilter", "personalize:DescribeFilter", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_recommender.py b/source/infrastructure/personalize/aws_lambda/functions/create_recommender.py index 1b8a93a..9449502 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_recommender.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_recommender.py @@ -42,6 +42,8 @@ def _set_permissions(self): "personalize:CreateRecommender", "personalize:ListRecommenders", "personalize:DescribeDatasetGroup", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_solution.py b/source/infrastructure/personalize/aws_lambda/functions/create_solution.py index 8c8aed5..7b686c9 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_solution.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_solution.py @@ -42,6 +42,8 @@ def _set_permissions(self): "personalize:CreateSolution", "personalize:ListSolutions", "personalize:DescribeDatasetGroup", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_solution_version.py b/source/infrastructure/personalize/aws_lambda/functions/create_solution_version.py index 1220ab6..38e2a8b 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_solution_version.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_solution_version.py @@ -43,6 +43,8 @@ def _set_permissions(self): "personalize:ListSolutionVersions", "personalize:DescribeSolution", "personalize:GetSolutionMetrics", + "personalize:TagResource", + "personalize:ListTagsForResource", ], effect=iam.Effect.ALLOW, resources=[ diff --git a/source/infrastructure/personalize/aws_lambda/layers/aws_solutions/requirements/requirements.txt b/source/infrastructure/personalize/aws_lambda/layers/aws_solutions/requirements/requirements.txt index 7b9477c..7506f91 100644 --- a/source/infrastructure/personalize/aws_lambda/layers/aws_solutions/requirements/requirements.txt +++ b/source/infrastructure/personalize/aws_lambda/layers/aws_solutions/requirements/requirements.txt @@ -4,4 +4,4 @@ avro==1.11.1 cronex==0.1.3.1 jmespath==1.0.1 parsedatetime==2.6 -boto3==1.25.5 +boto3==1.26.47 diff --git a/source/infrastructure/personalize/step_functions/batch_inference_jobs_fragment.py b/source/infrastructure/personalize/step_functions/batch_inference_jobs_fragment.py index fdff3bf..e9b0ea0 100644 --- a/source/infrastructure/personalize/step_functions/batch_inference_jobs_fragment.py +++ b/source/infrastructure/personalize/step_functions/batch_inference_jobs_fragment.py @@ -150,7 +150,7 @@ def __init__( items_path="$.solution.batchInferenceJobs", parameters={ "solutionVersionArn.$": "$.solution.solutionVersion.serviceConfig.solutionVersionArn", - "batchInferenceJob.$": "$$.Map.Item.Value", + "batchInferenceJob.$": "$$.Map.Item.Value", # NOSONAR (python:S1192) - string for clarity "batchInferenceJobName.$": f"States.Format('batch_{{}}_{{}}', $.solution.serviceConfig.name, {CURRENT_DATE_PATH})", "bucket.$": BUCKET_PATH, # NOSONAR (python:S1192) - string for clarity "currentDate.$": CURRENT_DATE_PATH, # NOSONAR (python:S1192) - string for clarity diff --git a/source/infrastructure/personalize/step_functions/batch_segment_jobs_fragment.py b/source/infrastructure/personalize/step_functions/batch_segment_jobs_fragment.py index f604c8a..15e24e7 100644 --- a/source/infrastructure/personalize/step_functions/batch_segment_jobs_fragment.py +++ b/source/infrastructure/personalize/step_functions/batch_segment_jobs_fragment.py @@ -148,7 +148,7 @@ def __init__( items_path="$.solution.batchSegmentJobs", parameters={ "solutionVersionArn.$": "$.solution.solutionVersion.serviceConfig.solutionVersionArn", - "batchSegmentJob.$": "$$.Map.Item.Value", + "batchSegmentJob.$": "$$.Map.Item.Value", # NOSONAR (python:S1192) - string for clarity "batchSegmentJobName.$": f"States.Format('batch_{{}}_{{}}', $.solution.serviceConfig.name, {CURRENT_DATE_PATH})", "bucket.$": BUCKET_PATH, # NOSONAR (python:S1192) - string for clarity "currentDate.$": CURRENT_DATE_PATH, # NOSONAR (python:S1192) - string for clarity diff --git a/source/infrastructure/personalize/step_functions/dataset_import_fragment.py b/source/infrastructure/personalize/step_functions/dataset_import_fragment.py index 3605e56..f0dd2e3 100644 --- a/source/infrastructure/personalize/step_functions/dataset_import_fragment.py +++ b/source/infrastructure/personalize/step_functions/dataset_import_fragment.py @@ -14,21 +14,20 @@ from aws_cdk import Duration from aws_cdk.aws_stepfunctions import ( - StateMachineFragment, - State, - TaskInput, - INextable, Choice, Condition, + INextable, JsonPath, Pass, + State, + StateMachineFragment, + TaskInput, ) from constructs import Construct - from personalize.aws_lambda.functions import ( CreateDataset, - CreateSchema, CreateDatasetImportJob, + CreateSchema, ) @@ -61,6 +60,13 @@ def __init__( "jobName.$": f"States.Format('dataset_import_{id.lower()}_{{}}', $.currentDate)", "datasetArn.$": f"$.datasets.{id.lower()}.dataset.serviceConfig.datasetArn", } + + service_input = { + "importMode": "FULL", + "publishAttributionMetricsToS3.$": "$.datasets.serviceConfig.publishAttributionMetricsToS3", + "tags.$": "$.datasets.serviceConfig.tags" + } + import_datasets_from_csv = create_dataset_import_job.state(self, f"Try {id} Dataset Import from CSV", payload=TaskInput.from_object({ "serviceConfig": { @@ -68,6 +74,7 @@ def __init__( "dataSource": { "dataLocation.$": f"States.Format('s3://{{}}/{{}}/{id.lower()}.csv', $.bucket.name, $.bucket.key)" # NOSONAR (python:S1192) - string for clarity }, + **service_input, }, "workflowConfig": { "maxAge.$": "$.datasetGroup.workflowConfig.maxAge", # NOSONAR (python:S1192) - string for clarity @@ -86,6 +93,7 @@ def __init__( "dataSource": { "dataLocation.$": f"States.Format('s3://{{}}/{{}}/{id.lower()}', $.bucket.name, $.bucket.key)" # NOSONAR (python:S1192) - string for clarity }, + **service_input }, "workflowConfig": { "maxAge.$": "$.datasetGroup.workflowConfig.maxAge", # NOSONAR (python:S1192) - string for clarity @@ -109,6 +117,7 @@ def __init__( "schemaArn.$": f"$.datasets.{id.lower()}.schema.serviceConfig.schemaArn", "datasetGroupArn.$": "$.datasetGroup.serviceConfig.datasetGroupArn", "datasetType": f"{id.lower()}", + "tags.$": f"$.datasets.{id.lower()}.dataset.serviceConfig.tags", }, "workflowConfig": { "maxAge.$": "$.datasetGroup.workflowConfig.maxAge", @@ -117,6 +126,7 @@ def __init__( }), result_path=f"$.datasets.{id.lower()}.dataset.serviceConfig", **retry_config)) + .next(import_datasets_from_prefix)) self._choice.otherwise( na_state diff --git a/source/infrastructure/personalize/step_functions/filter_fragment.py b/source/infrastructure/personalize/step_functions/filter_fragment.py index 52b8c67..b9b7644 100644 --- a/source/infrastructure/personalize/step_functions/filter_fragment.py +++ b/source/infrastructure/personalize/step_functions/filter_fragment.py @@ -65,7 +65,7 @@ def __init__( items_path="$.filters", parameters={ "datasetGroupArn.$": "$.datasetGroup.serviceConfig.datasetGroupArn", - "filter.$": "$$.Map.Item.Value", + "filter.$": "$$.Map.Item.Value", # NOSONAR (python:S1192) - string for clarity }, result_path=JsonPath.DISCARD, ) diff --git a/source/infrastructure/personalize/step_functions/solution_fragment.py b/source/infrastructure/personalize/step_functions/solution_fragment.py index d130c4c..d260a83 100644 --- a/source/infrastructure/personalize/step_functions/solution_fragment.py +++ b/source/infrastructure/personalize/step_functions/solution_fragment.py @@ -10,31 +10,32 @@ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### +import time +from datetime import datetime from typing import List, Optional from aws_cdk import Duration from aws_cdk.aws_stepfunctions import ( - StateMachineFragment, - State, - INextable, Choice, - Pass, - Map, Condition, + INextable, JsonPath, + Map, Parallel, + Pass, + State, StateMachine, + StateMachineFragment, ) -from constructs import Construct - from aws_solutions.scheduler.cdk.construct import Scheduler +from constructs import Construct from personalize.aws_lambda.functions import ( - CreateSolution, - CreateSolutionVersion, - CreateCampaign, CreateBatchInferenceJob, CreateBatchSegmentJob, + CreateCampaign, CreateRecommender, + CreateSolution, + CreateSolutionVersion, ) from personalize.step_functions.batch_inference_jobs_fragment import ( BatchInferenceJobsFragment, @@ -88,6 +89,7 @@ def __init__( input_path="$.datasetGroupArn", # NOSONAR (python:S1192) - string for clarity result_path="$.solution.serviceConfig.datasetGroupArn", ) + _prepare_recommender_input = Pass( self, "Prepare Recommender Input Data", @@ -108,7 +110,8 @@ def __init__( parameters={ "serviceConfig": { "solutionArn.$": "$.solution.serviceConfig.solutionArn", # NOSONAR (python:S1192) - string for clarity - "trainingMode": "FULL" + "trainingMode": "FULL", + "tags.$": "$.solution.serviceConfig.solutionVersion.tags" # NOSONAR (python:S1192) - string for clarity }, "workflowConfig": { "maxAge": "365 days", # do not create a new solution version on new file upload @@ -176,8 +179,9 @@ def __init__( "Set Solution Version ID", parameters={ "serviceConfig": { - "trainingMode.$": "$.solution.solutionVersion.serviceConfig.trainingMode", + "trainingMode.$": "$.solution.serviceConfig.solutionVersion.trainingMode", "solutionArn.$": "$.solution.solutionVersion.serviceConfig.solutionArn", # NOSONAR (python:S1192) - string for clarity + "tags.$": "$.solution.solutionVersion.serviceConfig.tags" # NOSONAR (python:S1192) - string for clarity }, "workflowConfig": { "maxAge.$": "$.solution.solutionVersion.workflowConfig.maxAge", @@ -224,7 +228,7 @@ def __init__( items_path="$.solution.campaigns", # NOSONAR (python:S1192) - string for clarity parameters={ "solutionVersionArn.$": "$.solution.solutionVersion.serviceConfig.solutionVersionArn", - "campaign.$": "$$.Map.Item.Value", + "campaign.$": "$$.Map.Item.Value", # NOSONAR (python:S1192) - string for clarity } ).iterator(_prepare_campaign_input .next(_create_campaign)) @@ -260,8 +264,9 @@ def __init__( "serviceConfig.$": "$.solution.serviceConfig", "solutionVersion": { "serviceConfig": { - "trainingMode": "FULL", + "trainingMode.$": "$.solution.serviceConfig.solutionVersion.trainingMode", "solutionArn.$": "$.solution.solutionVersion.serviceConfig.solutionArn", # NOSONAR (python:S1192) - string for clarity + "tags.$": "$.solution.solutionVersion.serviceConfig.tags" # NOSONAR (python:S1192) - string for clarity }, "workflowConfig": { "maxAge": MINIMUM_TIME @@ -295,6 +300,7 @@ def __init__( "serviceConfig": { "trainingMode": "UPDATE", "solutionArn.$": "$.solution.solutionVersion.serviceConfig.solutionArn", # NOSONAR (python:S1192) - string for clarity + "tags.$": "$.solution.solutionVersion.serviceConfig.tags" # NOSONAR (python:S1192) - string for clarity }, "workflowConfig": { "maxAge": MINIMUM_TIME, @@ -327,7 +333,7 @@ def __init__( parameters={ "datasetGroupArn.$": "$.datasetGroup.serviceConfig.datasetGroupArn", "datasetGroupName.$": "$.datasetGroup.serviceConfig.name", - "recommender.$": "$$.Map.Item.Value", + "recommender.$": "$$.Map.Item.Value", # NOSONAR (python:S1192) - string for clarity "bucket.$": BUCKET_PATH, "currentDate.$": CURRENT_DATE_PATH, # NOSONAR (python:S1192) - string for clarity } @@ -342,7 +348,7 @@ def __init__( parameters={ "datasetGroupArn.$": "$.datasetGroup.serviceConfig.datasetGroupArn", "datasetGroupName.$": "$.datasetGroup.serviceConfig.name", - "solution.$": "$$.Map.Item.Value", + "solution.$": "$$.Map.Item.Value", # NOSONAR (python:S1192) - string for clarity "bucket.$": BUCKET_PATH, "currentDate.$": CURRENT_DATE_PATH, # NOSONAR (python:S1192) - string for clarity } diff --git a/source/requirements-dev.txt b/source/requirements-dev.txt index f05b067..4724eb8 100644 --- a/source/requirements-dev.txt +++ b/source/requirements-dev.txt @@ -1,6 +1,6 @@ avro==1.11.1 black -boto3==1.25.5 +boto3==1.26.47 aws_cdk_lib==2.44.0 aws_solutions_constructs.aws_lambda_sns==2.25.0 aws-cdk.aws-servicecatalogappregistry-alpha==2.44.0a0 @@ -9,10 +9,10 @@ crhelper==2.0.11 cronex==0.1.3.1 moto==2.3.0 parsedatetime==2.6 -pytest -pytest-cov>=2.11.1 -pytest-env>=0.6.2 -pytest-mock>=3.5.1 +pytest>=7.2.0 +pytest-cov>=4.0.0 +pytest-env>=0.8.1 +pytest-mock>=3.10.0 pyyaml==5.4.1 responses~=0.17.0 tenacity==8.0.1 diff --git a/source/scheduler/README.md b/source/scheduler/README.md index 3c9f80c..5b42176 100644 --- a/source/scheduler/README.md +++ b/source/scheduler/README.md @@ -1,24 +1,25 @@ # AWS Solutions Step Functions Scheduler + ## Scheduling for AWS Step Functions -This tooling adds scheduling support for AWS Step Functions via a set of libraries and CDK packages. +This tooling adds scheduling support for AWS Step Functions via a set of libraries and CDK packages. -This README summarizes using the scheduler. +This README summarizes using the scheduler. ## Prerequisites Install this package. It requires at least: -- Python 3.7 -- AWS CDK version 2.7.0 or higher +- Python 3.9 +- AWS CDK version 2.44.0 or higher -To install the packages: +To install the packages: ``` pip install /scheduler/cdk # where is the path to the scheduler namespace package -pip install /scheduler/common # where is the path to the scheduler namespace package +pip install /scheduler/common # where is the path to the scheduler namespace package ``` - + ## 1. Add the scheduler to your CDK application ```python @@ -66,18 +67,18 @@ SchedulerFragment( # 3. Check the status of schedules using the included CLI This package also provides a CLI `aws-solutions-scheduler`. This CLI can be used to control the scheduler and establish -schedules for the [Maintaining Personalized Experiences with Machine Learning](https://aws.amazon.com/solutions/implementations/maintaining-personalized-experiences-with-ml/) -solution. +schedules for the [Maintaining Personalized Experiences with Machine Learning](https://aws.amazon.com/solutions/implementations/maintaining-personalized-experiences-with-ml/) +solution. ### Installation It is recommended that you perform the following steps in a dedicated virtual environment: ```shell -cd source -pip install --upgrade pip +cd source +pip install --upgrade pip pip install cdk_solution_helper_py/helpers_common -pip install scheduler/common +pip install scheduler/common ``` ### Usage @@ -104,7 +105,7 @@ Commands: #### Create new schedule(s) for an Amazon Personalize dataset group -Schedules for dataset import, solution version FULL and UPDATE retraining can be established using the CLI for dataset +Schedules for dataset import, solution version FULL and UPDATE retraining can be established using the CLI for dataset groups in Amazon Personalize. This example creates a weekly schedule for full dataset import (`-i`) and for full solution version retraining (-f) @@ -117,15 +118,16 @@ solution version retraining (-f) ```shell > aws-solutions-scheduler -s PersonalizeStack -r us-east-1 list ``` +
See sample result ```json { - "tasks": [ - "personalize-dataset-import-item-recommender", - "solution-maintenance-full-item-recommender-user-personalization" - ] + "tasks": [ + "personalize-dataset-import-item-recommender", + "solution-maintenance-full-item-recommender-user-personalization" + ] } ``` @@ -136,18 +138,19 @@ solution version retraining (-f) ```shell > aws-solutions-scheduler -s PersonalizeStack -r us-east-1 describe --task personalize-dataset-import-item-recommender ``` +
See sample result ```json { - "task": { - "active": true, - "name": "personalize-dataset-import-item-recommender", - "schedule": "cron(*/15 * * * ? *)", - "step_function": "arn:aws:states:us-east-1:111122223333:stateMachine:personalizestack-periodic-dataset-import-aaaaaaaaaaaa", - "version": "v1" - } + "task": { + "active": true, + "name": "personalize-dataset-import-item-recommender", + "schedule": "cron(*/15 * * * ? *)", + "step_function": "arn:aws:states:us-east-1:111122223333:stateMachine:personalizestack-periodic-dataset-import-aaaaaaaaaaaa", + "version": "v1" + } } ``` @@ -160,18 +163,19 @@ Deactivate schedules can be activated ```shell > aws-solutions-scheduler -s PersonalizeStack -r us-east-1 activate --task personalize-dataset-import-item-recommender ``` +
See sample result ```json { - "task": { - "active": true, - "name": "personalize-dataset-import-item-recommender", - "schedule": "cron(0 0 ? * 1 *)", - "step_function": "arn:aws:states:us-east-1:111122223333:stateMachine:personalizestack-periodic-dataset-import-aaaaaaaaaaaa", - "version": "v1" - } + "task": { + "active": true, + "name": "personalize-dataset-import-item-recommender", + "schedule": "cron(0 0 ? * 1 *)", + "step_function": "arn:aws:states:us-east-1:111122223333:stateMachine:personalizestack-periodic-dataset-import-aaaaaaaaaaaa", + "version": "v1" + } } ``` @@ -184,24 +188,25 @@ Deactivate schedules can be activated ```shell > aws-solutions-scheduler -s PersonalizeStack -r us-east-1 deactivate --task personalize-dataset-import-item-recommender ``` +
See sample result ```json { - "task": { - "active": false, - "name": "personalize-dataset-import-item-recommender", - "schedule": "cron(0 0 ? * 1 *)", - "step_function": "arn:aws:states:us-east-1:111122223333:stateMachine:personalizestack-periodic-dataset-import-aaaaaaaaaaaa", - "version": "v1" - } + "task": { + "active": false, + "name": "personalize-dataset-import-item-recommender", + "schedule": "cron(0 0 ? * 1 *)", + "step_function": "arn:aws:states:us-east-1:111122223333:stateMachine:personalizestack-periodic-dataset-import-aaaaaaaaaaaa", + "version": "v1" + } } ```
-*** +--- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -215,4 +220,4 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/source/scheduler/cdk/setup.py b/source/scheduler/cdk/setup.py index 3d2e178..908bce1 100644 --- a/source/scheduler/cdk/setup.py +++ b/source/scheduler/cdk/setup.py @@ -45,7 +45,7 @@ def get_version(): "pip>=22.3.1", "aws_cdk_lib==2.44.0", "Click==8.1.3", - "boto3==1.25.5", + "boto3==1.26.47", ], python_requires=">=3.9", classifiers=[ diff --git a/source/scheduler/common/setup.py b/source/scheduler/common/setup.py index bad7709..a0f6e7f 100644 --- a/source/scheduler/common/setup.py +++ b/source/scheduler/common/setup.py @@ -43,12 +43,12 @@ def get_version(): packages=setuptools.find_namespace_packages(exclude=["build*"]), install_requires=[ "pip>=22.3.1", - "aws-lambda-powertools==1.29.2", + "aws-lambda-powertools==2.10.0", "aws-xray-sdk==2.11.0", "aws-solutions-python==2.0.0", "click==8.1.3", "cronex==0.1.3.1", - "boto3==1.25.5", + "boto3==1.26.47", "requests==2.28.1", "crhelper==2.0.11", "rich==12.6.0", diff --git a/source/tests/aspects/test_personalize_app_stack.py b/source/tests/aspects/test_personalize_app_stack.py index cd579f0..92ab843 100644 --- a/source/tests/aspects/test_personalize_app_stack.py +++ b/source/tests/aspects/test_personalize_app_stack.py @@ -66,11 +66,11 @@ def test_service_catalog_registry_application(synth_template): "Tags": { "SOLUTION_ID": "SO0170", "SOLUTION_NAME": "Maintaining Personalized Experiences with Machine Learning", - "SOLUTION_VERSION": "v1.3.1", + "SOLUTION_VERSION": "v1.4.0", "Solutions:ApplicationType": "AWS-Solutions", "Solutions:SolutionID": "SO0170", "Solutions:SolutionName": "Maintaining Personalized Experiences with Machine Learning", - "Solutions:SolutionVersion": "v1.3.1", + "Solutions:SolutionVersion": "v1.4.0", }, }, ) diff --git a/source/tests/aws_lambda/__init__.py b/source/tests/aws_lambda/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_batch_inference_job/__init__.py b/source/tests/aws_lambda/create_batch_inference_job/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_batch_inference_job/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_batch_inference_job/test_batch_inference_job_handler.py b/source/tests/aws_lambda/create_batch_inference_job/test_batch_inference_job_handler.py index 1e3fff4..8bd76a4 100644 --- a/source/tests/aws_lambda/create_batch_inference_job/test_batch_inference_job_handler.py +++ b/source/tests/aws_lambda/create_batch_inference_job/test_batch_inference_job_handler.py @@ -11,17 +11,122 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +import os +import pytest from aws_lambda.create_batch_inference_job.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import BatchInferenceJob, SolutionVersion + +batch_inference_name = "mockBatchJob" +solution_version_name = "mockSolutionVersion" def test_create_batch_inference_job_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_batch_inference_tags(monkeypatch, personalize_stubber, notifier_stubber): + batch_inference_arn = BatchInferenceJob().arn(batch_inference_name) + solution_version_arn = SolutionVersion().arn(solution_version_name) + os.environ["ROLE_ARN"] = "roleArn" + personalize_stubber.add_response( + method="list_batch_inference_jobs", + expected_params={ + "solutionVersionArn": solution_version_arn, + }, + service_response={"batchInferenceJobs": []}, + ) + + personalize_stubber.add_response( + method="create_batch_inference_job", + expected_params={ + "jobName": batch_inference_name, + "solutionVersionArn": solution_version_arn, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": [ + {"tagKey": "batchInference-1", "tagValue": "batchInference-key-1"}, + ], + }, + service_response={"batchInferenceJobArn": batch_inference_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "jobName": batch_inference_name, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "tags": [{"tagKey": "batchInference-1", "tagValue": "batchInference-key-1"}], + "solutionVersionArn": solution_version_arn, + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + del os.environ["ROLE_ARN"] + + +@mock_sts +def test_bad_batch_inference_tags1(personalize_stubber): + os.environ["ROLE_ARN"] = "roleArn" + batch_inference_arn = BatchInferenceJob().arn(batch_inference_name) + solution_version_arn = SolutionVersion().arn(solution_version_name) + + personalize_stubber.add_response( + method="list_batch_inference_jobs", + expected_params={ + "solutionVersionArn": solution_version_arn, + }, + service_response={"batchInferenceJobs": []}, + ) + + personalize_stubber.add_response( + method="create_batch_inference_job", + expected_params={ + "jobName": batch_inference_name, + "solutionVersionArn": solution_version_arn, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": "bad data", + }, + service_response={"batchInferenceJobArn": batch_inference_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "jobName": batch_inference_name, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "tags": "bad data", + "solutionVersionArn": solution_version_arn, + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) + + del os.environ["ROLE_ARN"] diff --git a/source/tests/aws_lambda/create_batch_segment_job/__init__.py b/source/tests/aws_lambda/create_batch_segment_job/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_batch_segment_job/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_batch_segment_job/test_batch_segment_job_handler.py b/source/tests/aws_lambda/create_batch_segment_job/test_batch_segment_job_handler.py index 429ac0c..1c4294d 100644 --- a/source/tests/aws_lambda/create_batch_segment_job/test_batch_segment_job_handler.py +++ b/source/tests/aws_lambda/create_batch_segment_job/test_batch_segment_job_handler.py @@ -11,17 +11,123 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +import os +import pytest from aws_lambda.create_batch_segment_job.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import BatchSegmentJob, SolutionVersion + +batch_segment_name = "mockBatchJob" +solution_version_name = "mockSolutionVersion" def test_create_batch_segment_job_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_batch_segment_tags(monkeypatch, personalize_stubber, notifier_stubber): + os.environ["ROLE_ARN"] = "roleArn" + batch_segment_arn = BatchSegmentJob().arn(batch_segment_name) + solution_version_arn = SolutionVersion().arn(solution_version_name) + + personalize_stubber.add_response( + method="list_batch_segment_jobs", + expected_params={ + "solutionVersionArn": solution_version_arn, + }, + service_response={"batchSegmentJobs": []}, + ) + + personalize_stubber.add_response( + method="create_batch_segment_job", + expected_params={ + "jobName": batch_segment_name, + "solutionVersionArn": solution_version_arn, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": [ + {"tagKey": "batchSegment-1", "tagValue": "batchSegment-key-1"}, + ], + }, + service_response={"batchSegmentJobArn": batch_segment_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "jobName": batch_segment_name, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "tags": [{"tagKey": "batchSegment-1", "tagValue": "batchSegment-key-1"}], + "solutionVersionArn": solution_version_arn, + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + del os.environ["ROLE_ARN"] + + +@mock_sts +def test_bad_batch_segment_tags(personalize_stubber): + os.environ["ROLE_ARN"] = "roleArn" + batch_segment_arn = BatchSegmentJob().arn(batch_segment_name) + solution_version_arn = SolutionVersion().arn(solution_version_name) + + personalize_stubber.add_response( + method="list_batch_segment_jobs", + expected_params={ + "solutionVersionArn": solution_version_arn, + }, + service_response={"batchSegmentJobs": []}, + ) + + personalize_stubber.add_response( + method="create_batch_segment_job", + expected_params={ + "jobName": batch_segment_name, + "solutionVersionArn": solution_version_arn, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": "bad data", + }, + service_response={"batchSegmentJobArn": batch_segment_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "jobName": batch_segment_name, + "jobInput": {"s3DataSource": {"path": "s3Path1", "kmsKeyArn": "kmsArn"}}, + "jobOutput": {"s3DataDestination": {"path": "s3Path2", "kmsKeyArn": "kmsArn"}}, + "tags": "bad data", + "solutionVersionArn": solution_version_arn, + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) + + del os.environ["ROLE_ARN"] diff --git a/source/tests/aws_lambda/create_campaign/__init__.py b/source/tests/aws_lambda/create_campaign/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_campaign/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_campaign/test_create_campaign_handler.py b/source/tests/aws_lambda/create_campaign/test_create_campaign_handler.py index 613ee5a..125e64e 100644 --- a/source/tests/aws_lambda/create_campaign/test_create_campaign_handler.py +++ b/source/tests/aws_lambda/create_campaign/test_create_campaign_handler.py @@ -14,16 +14,11 @@ from datetime import datetime, timedelta import pytest +from aws_lambda.create_campaign.handler import CONFIG, RESOURCE, STATUS, lambda_handler +from botocore.exceptions import ParamValidationError from dateutil.parser import isoparse from dateutil.tz import tzlocal from moto import mock_sts - -from aws_lambda.create_campaign.handler import ( - lambda_handler, - RESOURCE, - STATUS, - CONFIG, -) from shared.exceptions import ResourcePending from shared.resource import Campaign, SolutionVersion @@ -38,14 +33,14 @@ def test_create_campaign(validate_handler_config): @mock_sts def test_describe_campaign_response(personalize_stubber, notifier_stubber): - c_name = "cp_name" + campaign_name = "mockCampaign" sv_arn = SolutionVersion().arn("unit_test", sv_id="12345678") personalize_stubber.add_response( method="describe_campaign", service_response={ "campaign": { - "campaignArn": Campaign().arn(c_name), - "name": c_name, + "campaignArn": Campaign().arn(campaign_name), + "name": campaign_name, "solutionVersionArn": sv_arn, "minProvisionedTPS": 1, "status": "ACTIVE", @@ -53,15 +48,16 @@ def test_describe_campaign_response(personalize_stubber, notifier_stubber): "creationDateTime": datetime.now(tz=tzlocal()) - timedelta(seconds=100), } }, - expected_params={"campaignArn": Campaign().arn(c_name)}, + expected_params={"campaignArn": Campaign().arn(campaign_name)}, ) result = lambda_handler( { "serviceConfig": { - "name": c_name, + "name": campaign_name, "solutionVersionArn": sv_arn, "minProvisionedTPS": 1, + "tags": [{"tagKey": "campaign-1", "tagValue": "campaign-key-1"}], }, "workflowConfig": { "maxAge": "365 days", @@ -77,28 +73,28 @@ def test_describe_campaign_response(personalize_stubber, notifier_stubber): @mock_sts def test_create_campaign_response(personalize_stubber, notifier_stubber): - c_name = "cp_name" + campaign_name = "mockCampaign" sv_arn = SolutionVersion().arn("unit_test", sv_id="12345678") personalize_stubber.add_client_error( method="describe_campaign", service_error_code="ResourceNotFoundException", - expected_params={"campaignArn": Campaign().arn(c_name)}, + expected_params={"campaignArn": Campaign().arn(campaign_name)}, ) personalize_stubber.add_response( method="create_campaign", expected_params={ - "name": c_name, + "name": campaign_name, "minProvisionedTPS": 1, "solutionVersionArn": sv_arn, }, - service_response={"campaignArn": Campaign().arn(c_name)}, + service_response={"campaignArn": Campaign().arn(campaign_name)}, ) with pytest.raises(ResourcePending): lambda_handler( { "serviceConfig": { - "name": c_name, + "name": campaign_name, "solutionVersionArn": sv_arn, "minProvisionedTPS": 1, }, @@ -116,15 +112,15 @@ def test_create_campaign_response(personalize_stubber, notifier_stubber): @mock_sts def test_update_campaign_start(personalize_stubber, notifier_stubber): - c_name = "cp_name" + campaign_name = "mockCampaign" sv_arn_old = SolutionVersion().arn("unit_test", sv_id="12345678") sv_arn_new = SolutionVersion().arn("unit_test", sv_id="01234567") personalize_stubber.add_response( method="describe_campaign", service_response={ "campaign": { - "campaignArn": Campaign().arn(c_name), - "name": c_name, + "campaignArn": Campaign().arn(campaign_name), + "name": campaign_name, "solutionVersionArn": sv_arn_old, "minProvisionedTPS": 1, "status": "ACTIVE", @@ -132,15 +128,15 @@ def test_update_campaign_start(personalize_stubber, notifier_stubber): "creationDateTime": datetime.now(tz=tzlocal()) - timedelta(seconds=100), } }, - expected_params={"campaignArn": Campaign().arn(c_name)}, + expected_params={"campaignArn": Campaign().arn(campaign_name)}, ) personalize_stubber.add_response( method="update_campaign", service_response={ - "campaignArn": Campaign().arn(c_name), + "campaignArn": Campaign().arn(campaign_name), }, expected_params={ - "campaignArn": Campaign().arn(c_name), + "campaignArn": Campaign().arn(campaign_name), "minProvisionedTPS": 1, "solutionVersionArn": sv_arn_new, }, @@ -149,11 +145,7 @@ def test_update_campaign_start(personalize_stubber, notifier_stubber): with pytest.raises(ResourcePending): lambda_handler( { - "serviceConfig": { - "name": c_name, - "solutionVersionArn": sv_arn_new, - "minProvisionedTPS": 1, - }, + "serviceConfig": {"name": campaign_name, "solutionVersionArn": sv_arn_new, "minProvisionedTPS": 1}, "workflowConfig": { "maxAge": "365 days", "timeStarted": "2021-10-19T15:18:32Z", @@ -168,15 +160,15 @@ def test_update_campaign_start(personalize_stubber, notifier_stubber): @mock_sts def test_describe_campaign_response_updating(personalize_stubber, notifier_stubber): - c_name = "cp_name" + campaign_name = "mockCampaign" sv_arn_old = SolutionVersion().arn("unit_test", sv_id="12345678") sv_arn_new = SolutionVersion().arn("unit_test", sv_id="01234567") personalize_stubber.add_response( method="describe_campaign", service_response={ "campaign": { - "campaignArn": Campaign().arn(c_name), - "name": c_name, + "campaignArn": Campaign().arn(campaign_name), + "name": campaign_name, "solutionVersionArn": sv_arn_old, "minProvisionedTPS": 1, "status": "ACTIVE", @@ -191,7 +183,7 @@ def test_describe_campaign_response_updating(personalize_stubber, notifier_stubb }, } }, - expected_params={"campaignArn": Campaign().arn(c_name)}, + expected_params={"campaignArn": Campaign().arn(campaign_name)}, ) personalize_stubber.add_client_error( method="update_campaign", @@ -199,13 +191,9 @@ def test_describe_campaign_response_updating(personalize_stubber, notifier_stubb ) with pytest.raises(ResourcePending): - result = lambda_handler( + lambda_handler( { - "serviceConfig": { - "name": c_name, - "solutionVersionArn": sv_arn_new, - "minProvisionedTPS": 1, - }, + "serviceConfig": {"name": campaign_name, "solutionVersionArn": sv_arn_new, "minProvisionedTPS": 1}, "workflowConfig": { "maxAge": "365 days", "timeStarted": "2021-10-19T15:18:32Z", @@ -220,14 +208,14 @@ def test_describe_campaign_response_updating(personalize_stubber, notifier_stubb @mock_sts def test_describe_campaign_response_updated(personalize_stubber, notifier_stubber): - c_name = "cp_name" + campaign_name = "mockCampaign" sv_arn_new = SolutionVersion().arn("unit_test", sv_id="01234567") personalize_stubber.add_response( method="describe_campaign", service_response={ "campaign": { - "campaignArn": Campaign().arn(c_name), - "name": c_name, + "campaignArn": Campaign().arn(campaign_name), + "name": campaign_name, "solutionVersionArn": sv_arn_new, "minProvisionedTPS": 1, "status": "ACTIVE", @@ -242,15 +230,16 @@ def test_describe_campaign_response_updated(personalize_stubber, notifier_stubbe }, } }, - expected_params={"campaignArn": Campaign().arn(c_name)}, + expected_params={"campaignArn": Campaign().arn(campaign_name)}, ) result = lambda_handler( { "serviceConfig": { - "name": c_name, + "name": campaign_name, "solutionVersionArn": sv_arn_new, "minProvisionedTPS": 1, + "tags": [{"tagKey": "campaign-1", "tagValue": "campaign-key-1"}], }, "workflowConfig": { "maxAge": "365 days", @@ -267,3 +256,53 @@ def test_describe_campaign_response_updated(personalize_stubber, notifier_stubbe last_updated = isoparse(notifier_stubber.get_resource_last_updated(Campaign(), {"campaign": result})) created = isoparse(notifier_stubber.get_resource_created(Campaign(), {"campaign": result})) assert (last_updated - created).seconds == 100 + + +@mock_sts +def test_bad_campaign_tags(personalize_stubber, notifier_stubber): + campaign_name = "mockCampaign" + sv_arn_new = SolutionVersion().arn("unit_test", sv_id="01234567") + personalize_stubber.add_response( + method="describe_campaign", + service_response={ + "campaign": { + "campaignArn": Campaign().arn(campaign_name), + "name": campaign_name, + "solutionVersionArn": sv_arn_new, + "minProvisionedTPS": 1, + "status": "ACTIVE", + "lastUpdatedDateTime": datetime.now(tzlocal()) - timedelta(seconds=1000), + "creationDateTime": datetime.now(tz=tzlocal()) - timedelta(seconds=1100), + "latestCampaignUpdate": { + "minProvisionedTPS": 1, + "solutionVersionArn": sv_arn_new, + "creationDateTime": datetime.now(tzlocal()) - timedelta(seconds=100), + "lastUpdatedDateTime": datetime.now(tzlocal()), + "status": "ACTIVE", + }, + } + }, + expected_params={"campaignArn": Campaign().arn(campaign_name)}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": campaign_name, + "solutionVersionArn": sv_arn_new, + "minProvisionedTPS": 1, + "tags": "bad data", + }, + "workflowConfig": { + "maxAge": "365 days", + "timeStarted": "2021-10-19T15:18:32Z", + }, + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/create_config/__init__.py b/source/tests/aws_lambda/create_config/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_config/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_config/test_create_config_handler.py b/source/tests/aws_lambda/create_config/test_create_config_handler.py index b9c9578..85542de 100644 --- a/source/tests/aws_lambda/create_config/test_create_config_handler.py +++ b/source/tests/aws_lambda/create_config/test_create_config_handler.py @@ -13,15 +13,15 @@ from aws_lambda.create_config.handler import lambda_handler from shared.resource import ( - DatasetGroup, - Dataset, - Solution, - Campaign, - SolutionVersion, BatchInferenceJob, + BatchSegmentJob, + Campaign, + Dataset, + DatasetGroup, EventTracker, Schema, - BatchSegmentJob, + Solution, + SolutionVersion, ) diff --git a/source/tests/aws_lambda/create_dataset/__init__.py b/source/tests/aws_lambda/create_dataset/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_dataset/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_dataset/test_dataset_handler.py b/source/tests/aws_lambda/create_dataset/test_dataset_handler.py index 80ac0d6..be018a5 100644 --- a/source/tests/aws_lambda/create_dataset/test_dataset_handler.py +++ b/source/tests/aws_lambda/create_dataset/test_dataset_handler.py @@ -11,16 +11,107 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### +from datetime import datetime, timedelta + import pytest +from aws_lambda.create_dataset.handler import CONFIG, RESOURCE, lambda_handler +from botocore.exceptions import ParamValidationError +from dateutil.tz import tzlocal +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import Dataset, DatasetGroup -from aws_lambda.create_dataset.handler import ( - lambda_handler, - RESOURCE, - CONFIG, -) +dataset_group_name = "mockDatasetGroup" +dataset_name = "mockDataset" def test_create_dataset_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_dataset_tags(personalize_stubber, notifier_stubber): + dataset_arn = Dataset().arn(dataset_name) + dataset_group_arn = DatasetGroup().arn(dataset_group_name) + + personalize_stubber.add_response( + method="list_datasets", + expected_params={"datasetGroupArn": dataset_group_arn}, + service_response={"datasets": []}, + ) + + personalize_stubber.add_response( + method="create_dataset", + expected_params={ + "name": dataset_name, + "schemaArn": "schemaArn", + "datasetGroupArn": dataset_group_arn, + "datasetType": "INTERACTIONS", + "tags": [ + {"tagKey": "dataset-1", "tagValue": "dataset-key-1"}, + ], + }, + service_response={"datasetArn": dataset_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "name": dataset_name, + "schemaArn": "schemaArn", + "datasetGroupArn": dataset_group_arn, + "datasetType": "INTERACTIONS", + "tags": [{"tagKey": "dataset-1", "tagValue": "dataset-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_bad_dataset_tags(personalize_stubber): + dataset_arn = Dataset().arn(dataset_name) + dataset_group_arn = DatasetGroup().arn(dataset_group_name) + + personalize_stubber.add_response( + method="list_datasets", + expected_params={"datasetGroupArn": dataset_group_arn}, + service_response={"datasets": []}, + ) + + personalize_stubber.add_response( + method="create_dataset", + expected_params={ + "name": dataset_name, + "schemaArn": "schemaArn", + "datasetGroupArn": dataset_group_arn, + "datasetType": "INTERACTIONS", + "tags": "bad data", + }, + service_response={"datasetArn": dataset_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": dataset_name, + "schemaArn": "schemaArn", + "datasetGroupArn": dataset_group_arn, + "datasetType": "INTERACTIONS", + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/create_dataset_group/__init__.py b/source/tests/aws_lambda/create_dataset_group/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_dataset_group/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_dataset_group/test_dataset_group_handler.py b/source/tests/aws_lambda/create_dataset_group/test_dataset_group_handler.py index caedbd7..22846f9 100644 --- a/source/tests/aws_lambda/create_dataset_group/test_dataset_group_handler.py +++ b/source/tests/aws_lambda/create_dataset_group/test_dataset_group_handler.py @@ -11,17 +11,150 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +from datetime import datetime, timedelta +import pytest from aws_lambda.create_dataset_group.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from dateutil.tz import tzlocal +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.personalize_service import Personalize +from shared.resource import DatasetGroup + +dataset_group_name = "mockDatasetGroup" def test_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_dsg_tags(personalize_stubber, notifier_stubber): + """ + The typical workflow is to describe, then create, then raise ResourcePending + """ + dataset_group_arn = DatasetGroup().arn(dataset_group_name) + personalize_stubber.add_client_error( + method="describe_dataset_group", + service_error_code="ResourceNotFoundException", + expected_params={"datasetGroupArn": dataset_group_arn}, + ) + personalize_stubber.add_response( + method="create_dataset_group", + expected_params={ + "name": dataset_group_name, + "tags": [ + {"tagKey": "datasetGroup-1", "tagValue": "datasetGroup-key-1"}, + ], + }, + service_response={"datasetGroupArn": dataset_group_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "name": dataset_group_name, + "tags": [{"tagKey": "datasetGroup-1", "tagValue": "datasetGroup-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_dsg_bad_tags(personalize_stubber): + """ + The typical workflow is to describe, then create, then raise ResourcePending + """ + dataset_group_arn = DatasetGroup().arn(dataset_group_name) + personalize_stubber.add_client_error( + method="describe_dataset_group", + service_error_code="ResourceNotFoundException", + expected_params={"datasetGroupArn": dataset_group_arn}, + ) + personalize_stubber.add_response( + method="create_dataset_group", + expected_params={ + "name": dataset_group_name, + "tags": "bad data", + }, + service_response={"datasetGroupArn": dataset_group_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": dataset_group_name, + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) + + +@mock_sts +def test_dsg_list_tags(personalize_stubber, notifier_stubber): + """ + The typical workflow is to describe, then create, then raise ResourcePending + """ + dsg_name = "mockDatasetGroup" + dataset_group_arn = DatasetGroup().arn(dataset_group_name) + personalize_stubber.add_response( + method="describe_dataset_group", + service_response={ + "datasetGroup": { + "name": dsg_name, + "datasetGroupArn": dataset_group_arn, + "status": "ACTIVE", + "lastUpdatedDateTime": datetime.now(tzlocal()), + "creationDateTime": datetime.now(tz=tzlocal()) - timedelta(seconds=100), + "roleArn": "roleArn", + "kmsKeyArn": "kmsArn", + } + }, + expected_params={"datasetGroupArn": dataset_group_arn}, + ) + + personalize_stubber.add_response( + method="list_tags_for_resource", + expected_params={"resourceArn": dataset_group_arn}, + service_response={ + "tags": [ + {"tagKey": "datasetGroup-1", "tagValue": "datasetGroup-key-1"}, + ] + }, + ) + + lambda_handler( + { + "serviceConfig": { + "name": dsg_name, + "tags": [{"tagKey": "datasetGroup-1", "tagValue": "datasetGroup-key-1"}], + } + }, + None, + ) + + cli = Personalize() + arn = DatasetGroup().arn(dsg_name) + assert cli.list_tags_for_resource(resourceArn=arn) == { + "tags": [{"tagKey": "datasetGroup-1", "tagValue": "datasetGroup-key-1"}] + } diff --git a/source/tests/aws_lambda/create_dataset_import_job/__init__.py b/source/tests/aws_lambda/create_dataset_import_job/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_dataset_import_job/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_dataset_import_job/test_dataset_import_job_handler.py b/source/tests/aws_lambda/create_dataset_import_job/test_dataset_import_job_handler.py index 30e483e..5d0591f 100644 --- a/source/tests/aws_lambda/create_dataset_import_job/test_dataset_import_job_handler.py +++ b/source/tests/aws_lambda/create_dataset_import_job/test_dataset_import_job_handler.py @@ -11,17 +11,134 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +import os +import pytest from aws_lambda.create_dataset_import_job.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import Dataset, DatasetGroup, DatasetImportJob + +dataset_name = "mockDataset" +dataset_arn = Dataset().arn(dataset_name) +dataset_import_arn = DatasetImportJob().arn("mockDatasetImport") +dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") def test_create_dataset_import_job_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_data_import_tags(mocker, personalize_stubber, notifier_stubber): + os.environ["ROLE_ARN"] = "roleArn" + dataset_arn = Dataset().arn(dataset_name) + dataset_import_arn = DatasetImportJob().arn("mockDatasetImport") + + personalize_stubber.add_response( + method="list_dataset_import_jobs", + expected_params={"datasetArn": dataset_arn}, + service_response={"datasetImportJobs": []}, + ) + + personalize_stubber.add_response( + method="create_dataset_import_job", + expected_params={ + "jobName": dataset_name, + "datasetArn": dataset_arn, + "dataSource": {"dataLocation": "s3://path/to/file"}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": [ + {"tagKey": "datasetImport-1", "tagValue": "datasetImport-key-1"}, + ], + "importMode": "FULL", + "publishAttributionMetricsToS3": True, + }, + service_response={"datasetImportJobArn": dataset_import_arn}, + ) + + mocker.patch("shared.s3.S3.exists", True) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "jobName": dataset_name, + "datasetArn": dataset_arn, + "dataSource": {"dataLocation": "s3://path/to/file"}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": [ + {"tagKey": "datasetImport-1", "tagValue": "datasetImport-key-1"}, + ], + "importMode": "FULL", + "publishAttributionMetricsToS3": True, + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + del os.environ["ROLE_ARN"] + + +@mock_sts +def test_bad_data_import_tags(mocker, personalize_stubber): + dataset_arn = Dataset().arn(dataset_name) + dataset_import_arn = DatasetImportJob().arn("mockDatasetImport") + + os.environ["ROLE_ARN"] = "roleArn" + + personalize_stubber.add_response( + method="list_dataset_import_jobs", + expected_params={"datasetArn": dataset_arn}, + service_response={"datasetImportJobs": []}, + ) + + personalize_stubber.add_response( + method="create_dataset_import_job", + expected_params={ + "jobName": dataset_name, + "datasetArn": dataset_arn, + "dataSource": {"dataLocation": "s3://path/to/file"}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": "bad data", + "importMode": "FULL", + "publishAttributionMetricsToS3": True, + }, + service_response={"datasetImportJobArn": dataset_import_arn}, + ) + + mocker.patch("shared.s3.S3.exists", True) + + try: + lambda_handler( + { + "serviceConfig": { + "jobName": dataset_name, + "datasetArn": dataset_arn, + "dataSource": {"dataLocation": "s3://path/to/file"}, + "roleArn": os.getenv("ROLE_ARN"), + "tags": "bad data", + "importMode": "FULL", + "publishAttributionMetricsToS3": True, + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) + + del os.environ["ROLE_ARN"] diff --git a/source/tests/aws_lambda/create_event_tracker/__init__.py b/source/tests/aws_lambda/create_event_tracker/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_event_tracker/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_event_tracker/test_create_event_tracker_handler.py b/source/tests/aws_lambda/create_event_tracker/test_create_event_tracker_handler.py index 449ceea..352b707 100644 --- a/source/tests/aws_lambda/create_event_tracker/test_create_event_tracker_handler.py +++ b/source/tests/aws_lambda/create_event_tracker/test_create_event_tracker_handler.py @@ -11,17 +11,108 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +import os +import pytest from aws_lambda.create_event_tracker.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import DatasetGroup, EventTracker + +etracker_name = "mockEventTracker" +event_tracker_arn = EventTracker().arn(etracker_name) +dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") def test_create_event_tracker(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_event_tracker_tags(personalize_stubber, notifier_stubber): + event_tracker_arn = EventTracker().arn(etracker_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + + personalize_stubber.add_response( + method="list_event_trackers", + expected_params={ + "datasetGroupArn": dataset_group_arn, + }, + service_response={"eventTrackers": []}, + ) + + personalize_stubber.add_response( + method="create_event_tracker", + expected_params={ + "name": etracker_name, + "datasetGroupArn": dataset_group_arn, + "tags": [ + {"tagKey": "et-1", "tagValue": "et-key-1"}, + ], + }, + service_response={"eventTrackerArn": event_tracker_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "name": etracker_name, + "datasetGroupArn": dataset_group_arn, + "tags": [{"tagKey": "et-1", "tagValue": "et-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_bad_event_tracker_tags(personalize_stubber): + event_tracker_arn = EventTracker().arn(etracker_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + + personalize_stubber.add_response( + method="list_event_trackers", + expected_params={ + "datasetGroupArn": dataset_group_arn, + }, + service_response={"eventTrackers": []}, + ) + + personalize_stubber.add_response( + method="create_event_tracker", + expected_params={ + "name": etracker_name, + "datasetGroupArn": dataset_group_arn, + "tags": "bad data", + }, + service_response={"eventTrackerArn": event_tracker_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": etracker_name, + "datasetGroupArn": dataset_group_arn, + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/create_filter/__init__.py b/source/tests/aws_lambda/create_filter/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_filter/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_filter/test_create_filter_handler.py b/source/tests/aws_lambda/create_filter/test_create_filter_handler.py index f605cdc..d32a1a9 100644 --- a/source/tests/aws_lambda/create_filter/test_create_filter_handler.py +++ b/source/tests/aws_lambda/create_filter/test_create_filter_handler.py @@ -11,17 +11,101 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### +import os + import pytest +from aws_lambda.create_filter.handler import CONFIG, RESOURCE, STATUS, lambda_handler +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import DatasetGroup, Filter -from aws_lambda.create_filter.handler import ( - lambda_handler, - RESOURCE, - STATUS, - CONFIG, -) +filter_name = "mockFilter" def test_create_filter(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_filter_tags(personalize_stubber, notifier_stubber): + filter_arn = Filter().arn(filter_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + + personalize_stubber.add_client_error( + method="describe_filter", + service_error_code="ResourceNotFoundException", + expected_params={"filterArn": filter_arn}, + ) + + personalize_stubber.add_response( + method="create_filter", + expected_params={ + "name": filter_name, + "datasetGroupArn": dataset_group_arn, + "filterExpression": "SOME-EXPRESSION", + "tags": [ + {"tagKey": "filter-1", "tagValue": "filter-key-1"}, + ], + }, + service_response={"filterArn": filter_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "name": filter_name, + "datasetGroupArn": dataset_group_arn, + "filterExpression": "SOME-EXPRESSION", + "tags": [{"tagKey": "filter-1", "tagValue": "filter-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_bad_filter_tags(personalize_stubber): + filter_arn = Filter().arn(filter_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + + personalize_stubber.add_client_error( + method="describe_filter", + service_error_code="ResourceNotFoundException", + expected_params={"filterArn": filter_arn}, + ) + + personalize_stubber.add_response( + method="create_filter", + expected_params={ + "name": filter_name, + "datasetGroupArn": dataset_group_arn, + "filterExpression": "SOME-EXPRESSION", + "tags": "bad data", + }, + service_response={"filterArn": filter_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": filter_name, + "datasetGroupArn": dataset_group_arn, + "filterExpression": "SOME-EXPRESSION", + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/create_recommender/__init__.py b/source/tests/aws_lambda/create_recommender/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_recommender/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_recommender/test_create_recommender_handler.py b/source/tests/aws_lambda/create_recommender/test_create_recommender_handler.py index 4352784..d9a3f36 100644 --- a/source/tests/aws_lambda/create_recommender/test_create_recommender_handler.py +++ b/source/tests/aws_lambda/create_recommender/test_create_recommender_handler.py @@ -11,17 +11,104 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +import os +import pytest from aws_lambda.create_recommender.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import DatasetGroup, Recommender + +recommender_name = "recommender-1" def test_create_recommender_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_recommender_tags(personalize_stubber, notifier_stubber): + recommender_arn = Recommender().arn(recommender_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + personalize_stubber.add_client_error( + method="describe_recommender", + service_error_code="ResourceNotFoundException", + expected_params={"recommenderArn": recommender_arn}, + ) + + personalize_stubber.add_response( + method="create_recommender", + expected_params={ + "name": recommender_name, + "datasetGroupArn": dataset_group_arn, + "recipeArn": "recipeArn", + "tags": [ + {"tagKey": "recommender-1", "tagValue": "recommender-key-1"}, + ], + }, + service_response={"recommenderArn": recommender_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "name": recommender_name, + "datasetGroupArn": dataset_group_arn, + "recipeArn": "recipeArn", + "tags": [{"tagKey": "recommender-1", "tagValue": "recommender-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_bad_recommender_tags(personalize_stubber): + recommender_arn = Recommender().arn(recommender_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + personalize_stubber.add_client_error( + method="describe_recommender", + service_error_code="ResourceNotFoundException", + expected_params={"recommenderArn": recommender_arn}, + ) + + personalize_stubber.add_response( + method="create_recommender", + expected_params={ + "name": recommender_name, + "datasetGroupArn": dataset_group_arn, + "recipeArn": "recipeArn", + "tags": "bad data", + }, + service_response={"recommenderArn": recommender_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": recommender_name, + "datasetGroupArn": dataset_group_arn, + "recipeArn": "recipeArn", + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/create_schema/__init__.py b/source/tests/aws_lambda/create_schema/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_schema/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_schema/create_schema_handler.py b/source/tests/aws_lambda/create_schema/create_schema_handler.py index 0d2184a..83b2260 100644 --- a/source/tests/aws_lambda/create_schema/create_schema_handler.py +++ b/source/tests/aws_lambda/create_schema/create_schema_handler.py @@ -12,12 +12,7 @@ # ###################################################################################################################### import pytest - -from aws_lambda.create_schema.handler import ( - lambda_handler, - RESOURCE, - CONFIG, -) +from aws_lambda.create_schema.handler import CONFIG, RESOURCE, lambda_handler def test_create_schema_handler(validate_handler_config): diff --git a/source/tests/aws_lambda/create_solution/__init__.py b/source/tests/aws_lambda/create_solution/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_solution/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_solution/test_create_solution_handler.py b/source/tests/aws_lambda/create_solution/test_create_solution_handler.py index e4b8b64..e6feb66 100644 --- a/source/tests/aws_lambda/create_solution/test_create_solution_handler.py +++ b/source/tests/aws_lambda/create_solution/test_create_solution_handler.py @@ -11,17 +11,101 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### +import os + import pytest +from aws_lambda.create_solution.handler import CONFIG, RESOURCE, STATUS, lambda_handler +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import ResourcePending +from shared.resource import DatasetGroup, Solution -from aws_lambda.create_solution.handler import ( - lambda_handler, - RESOURCE, - STATUS, - CONFIG, -) +solution_name = "mockSolution" def test_create_solution(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_solution_tags(personalize_stubber, notifier_stubber): + solution_arn = Solution().arn(solution_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + + personalize_stubber.add_client_error( + method="describe_solution", + service_error_code="ResourceNotFoundException", + expected_params={"solutionArn": solution_arn}, + ) + + personalize_stubber.add_response( + method="create_solution", + expected_params={ + "name": solution_name, + "recipeArn": "recipeArn", + "datasetGroupArn": dataset_group_arn, + "tags": [ + {"tagKey": "solution-1", "tagValue": "solution-key-1"}, + ], + }, + service_response={"solutionArn": solution_arn}, + ) + + with pytest.raises(ResourcePending): + lambda_handler( + { + "serviceConfig": { + "name": solution_name, + "recipeArn": "recipeArn", + "datasetGroupArn": dataset_group_arn, + "tags": [{"tagKey": "solution-1", "tagValue": "solution-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_bad_solution_tags(personalize_stubber): + solution_arn = Solution().arn(solution_name) + dataset_group_arn = DatasetGroup().arn("mockDatasetGroup") + + personalize_stubber.add_client_error( + method="describe_solution", + service_error_code="ResourceNotFoundException", + expected_params={"solutionArn": solution_arn}, + ) + + personalize_stubber.add_response( + method="create_solution", + expected_params={ + "name": solution_name, + "recipeArn": "recipeArn", + "datasetGroupArn": dataset_group_arn, + "tags": "bad data", + }, + service_response={"solutionArn": solution_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "name": solution_name, + "recipeArn": "recipeArn", + "datasetGroupArn": dataset_group_arn, + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/create_solution_version/__init__.py b/source/tests/aws_lambda/create_solution_version/__init__.py new file mode 100644 index 0000000..ef2f9eb --- /dev/null +++ b/source/tests/aws_lambda/create_solution_version/__init__.py @@ -0,0 +1,12 @@ +# ###################################################################################################################### +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # +# with the License. You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed # +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # +# the specific language governing permissions and limitations under the License. # +# ###################################################################################################################### diff --git a/source/tests/aws_lambda/create_solution_version/test_create_solution_version_handler.py b/source/tests/aws_lambda/create_solution_version/test_create_solution_version_handler.py index 9e3c1d2..5a2f91e 100644 --- a/source/tests/aws_lambda/create_solution_version/test_create_solution_version_handler.py +++ b/source/tests/aws_lambda/create_solution_version/test_create_solution_version_handler.py @@ -11,17 +11,102 @@ # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### -import pytest +import os +import pytest from aws_lambda.create_solution_version.handler import ( - lambda_handler, + CONFIG, RESOURCE, STATUS, - CONFIG, + lambda_handler, ) +from botocore.exceptions import ParamValidationError +from moto import mock_sts +from shared.exceptions import SolutionVersionPending +from shared.resource import Solution, SolutionVersion + +solution_version_name = "abcdefghi" # hash name of the solution_version def test_create_solution_version_handler(validate_handler_config): validate_handler_config(RESOURCE, CONFIG, STATUS) with pytest.raises(ValueError): lambda_handler({}, None) + + +@mock_sts +def test_solutionv_tags(personalize_stubber, notifier_stubber): + solutionv_arn = SolutionVersion().arn(solution_version_name) + solution_arn = Solution().arn("solName") + + personalize_stubber.add_response( + method="list_solution_versions", + expected_params={"solutionArn": solution_arn}, + service_response={"solutionVersions": []}, + ) + + personalize_stubber.add_response( + method="create_solution_version", + expected_params={ + "solutionArn": solution_arn, + "trainingMode": "FULL", + "tags": [ + {"tagKey": "solutionVersion-1", "tagValue": "solutionVersion-key-1"}, + ], + }, + service_response={"solutionVersionArn": solutionv_arn}, + ) + + with pytest.raises(SolutionVersionPending): + lambda_handler( + { + "serviceConfig": { + "solutionArn": solution_arn, + "trainingMode": "FULL", + "tags": [{"tagKey": "solutionVersion-1", "tagValue": "solutionVersion-key-1"}], + } + }, + None, + ) + + assert notifier_stubber.has_notified_for_creation + assert notifier_stubber.latest_notification_status == "CREATING" + + +@mock_sts +def test_solutionv_bad_tags(personalize_stubber): + solutionv_arn = SolutionVersion().arn(solution_version_name) + solution_arn = Solution().arn("solName") + + personalize_stubber.add_response( + method="list_solution_versions", + expected_params={"solutionArn": solution_arn}, + service_response={"solutionVersions": []}, + ) + + personalize_stubber.add_response( + method="create_solution_version", + expected_params={ + "solutionArn": solution_arn, + "trainingMode": "FULL", + "tags": "bad data", + }, + service_response={"solutionVersionArn": solutionv_arn}, + ) + + try: + lambda_handler( + { + "serviceConfig": { + "solutionArn": solution_arn, + "trainingMode": "FULL", + "tags": "bad data", + } + }, + None, + ) + except ParamValidationError as exp: + assert ( + exp.kwargs["report"] + == "Invalid type for parameter tags, value: bad data, type: , valid types: , " + ) diff --git a/source/tests/aws_lambda/s3_event/test_s3_event_handler.py b/source/tests/aws_lambda/s3_event/test_s3_event_handler.py index 381015f..7f733d7 100644 --- a/source/tests/aws_lambda/s3_event/test_s3_event_handler.py +++ b/source/tests/aws_lambda/s3_event/test_s3_event_handler.py @@ -15,10 +15,9 @@ import boto3 import pytest -from moto import mock_s3, mock_stepfunctions, mock_sns, mock_sts - from aws_lambda.s3_event.handler import lambda_handler from aws_solutions.core.helpers import _helpers_service_clients +from moto import mock_s3, mock_sns, mock_stepfunctions, mock_sts @pytest.fixture @@ -169,3 +168,35 @@ def test_s3_event_handler_bad_config(s3_event, sns_mocked, s3_mocked, stepfuncti stateMachineArn=environ.get("STATE_MACHINE_ARN"), ) assert len(executions["executions"]) == 0 + + +@mock_sts +def test_s3_event_handler_bad_tags(s3_event, sns_mocked, s3_mocked, stepfunctions_mocked): + s3_mocked.put_object( + Bucket="bucket-name", + Key="train/object-key.json", + Body=json.dumps({"tags": [{"tagKeys": "tagKey", "tagValue": "tagValue"}]}), + ) + lambda_handler(s3_event, None) + + # ensure no executions started + executions = stepfunctions_mocked.list_executions( + stateMachineArn=environ.get("STATE_MACHINE_ARN"), + ) + assert len(executions["executions"]) == 0 + + +@mock_sts +def test_s3_event_handler_more_bad_tags(s3_event, sns_mocked, s3_mocked, stepfunctions_mocked): + s3_mocked.put_object( + Bucket="bucket-name", + Key="train/object-key.json", + Body=json.dumps({"tags": "bad data"}), + ) + lambda_handler(s3_event, None) + + # ensure no executions started + executions = stepfunctions_mocked.list_executions( + stateMachineArn=environ.get("STATE_MACHINE_ARN"), + ) + assert len(executions["executions"]) == 0 diff --git a/source/tests/aws_lambda/sns_notification/test_sns_notification.py b/source/tests/aws_lambda/sns_notification/test_sns_notification.py index 185707a..70b757c 100644 --- a/source/tests/aws_lambda/sns_notification/test_sns_notification.py +++ b/source/tests/aws_lambda/sns_notification/test_sns_notification.py @@ -16,9 +16,8 @@ import boto3 import pytest -from moto import mock_sns, mock_sqs - from aws_lambda.sns_notification.handler import lambda_handler +from moto import mock_sns, mock_sqs TRACE_ID = "1-57f5498f-d91047849216d0f2ea3b6442" @@ -30,7 +29,6 @@ def sqs_mock(): with mock_sqs(): with mock_sns(): - cli = boto3.client("sns") cli.create_topic(Name=topic_name) @@ -77,7 +75,10 @@ def test_sns_notification(context, sqs_mock): url = sqs_mock.get_queue_url(QueueName="TestQueue")["QueueUrl"] msg = json.loads( json.loads( - sqs_mock.receive_message(QueueUrl=url, MaxNumberOfMessages=1,)["Messages"][ + sqs_mock.receive_message( + QueueUrl=url, + MaxNumberOfMessages=1, + )["Messages"][ 0 ]["Body"] )["Message"] @@ -111,7 +112,10 @@ def test_sns_notification_trace(sqs_mock, trace_enabled, context): url = sqs_mock.get_queue_url(QueueName="TestQueue")["QueueUrl"] msg = json.loads( json.loads( - sqs_mock.receive_message(QueueUrl=url, MaxNumberOfMessages=1,)["Messages"][ + sqs_mock.receive_message( + QueueUrl=url, + MaxNumberOfMessages=1, + )["Messages"][ 0 ]["Body"] )["Message"] diff --git a/source/tests/aws_lambda/test_personalize_service.py b/source/tests/aws_lambda/test_personalize_service.py index c0c09ac..becdd1c 100644 --- a/source/tests/aws_lambda/test_personalize_service.py +++ b/source/tests/aws_lambda/test_personalize_service.py @@ -17,17 +17,17 @@ import boto3 import pytest -from dateutil import tz -from dateutil.tz import tzlocal -from moto import mock_s3, mock_sts - from aws_lambda.shared.personalize_service import ( S3, - Personalize, Configuration, + Personalize, get_duplicates, ) -from shared.exceptions import ResourceNeedsUpdate, ResourceFailed +from dateutil import tz +from dateutil.tz import tzlocal +from moto import mock_s3, mock_sts +from moto.core import ACCOUNT_ID +from shared.exceptions import ResourceFailed, ResourceNeedsUpdate from shared.personalize.service_model import ServiceModel from shared.resource import Campaign @@ -58,7 +58,6 @@ def describe_solution_version_response(): "solutionVersionArn": f'arn:aws:personalize:us-east-1:{"1" * 12}:solution/personalize-integration-test-ranking/dfcd6f6e', "solutionArn": f'arn:aws:personalize:us-east-1:{"1" * 12}:solution/personalize-integration-test-ranking', "performHPO": False, - "performAutoML": False, "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization", "datasetGroupArn": f'arn:aws:personalize:us-east-1:{"1" * 12}:dataset-group/personalize-integration-test', "solutionConfig": {}, @@ -239,11 +238,11 @@ def test_service_model(personalize_stubber): ) sm = ServiceModel(cli) - assert sm.owned_by(filter_arn_1, dataset_group_arn_1) assert sm.owned_by(campaign_arn_1, dataset_group_name_1) assert sm.owned_by(filter_arn_2, dataset_group_arn_2) assert sm.owned_by(campaign_arn_2, dataset_group_name_2) + for arn in [ dataset_group_arn_1, dataset_group_arn_2, @@ -265,6 +264,14 @@ def test_configuration_valid(configuration_path): assert validates +@mock_sts +def test_configuration_valid(tags_configuration_path): + cfg = Configuration() + cfg.load(tags_configuration_path) + validates = cfg.validate() + assert validates + + @mock_sts def test_configuration_empty(config_empty): cfg = Configuration() @@ -492,6 +499,9 @@ def test_solution_version_update_validation(): "serviceConfig": { "name": "valid", "recipeArn": "arn:aws:personalize:::recipe/aws-sims", + "solutionVersion": { + "tags": [{"tagKey": "solv-2", "tagValue": "solv-key-2"}], + }, }, "workflowConfig": { "schedules": { @@ -503,6 +513,7 @@ def test_solution_version_update_validation(): "serviceConfig": { "name": "valid", "recipeArn": "arn:aws:personalize:::recipe/aws-hrnn-coldstart", + "tags": [{"tagKey": "sol-3", "tagValue": "sol-key-3"}], }, "workflowConfig": { "schedules": { @@ -528,3 +539,566 @@ def test_solution_version_update_validation(): cfg._validate_solution_update() assert len(cfg._configuration_errors) == 1 assert cfg._configuration_errors[0].startswith("solution invalid does not support") + + +@mock_sts +def test_dataset_defaults(configuration_path): + """ + Ensures that defaults are set for the fields for step-functions to pass. + """ + cfg = Configuration() + cfg.load(configuration_path) + + validates = cfg.validate() + assert validates + assert len(cfg._configuration_errors) == 0 + + # datasetGroup defaults + assert cfg.config_dict["datasetGroup"]["serviceConfig"]["tags"] == [] + + # dataset-import defaults + assert cfg.config_dict["datasets"]["serviceConfig"]["importMode"] == "FULL" + assert cfg.config_dict["datasets"]["serviceConfig"]["tags"] == [] + + assert cfg.config_dict["datasets"]["serviceConfig"]["publishAttributionMetricsToS3"] == False + + # dataset defaults + assert cfg.config_dict["datasets"]["users"]["dataset"]["serviceConfig"]["tags"] == [] + assert cfg.config_dict["datasets"]["interactions"]["dataset"]["serviceConfig"]["tags"] == [] + assert cfg.config_dict["datasets"]["items"]["dataset"]["serviceConfig"]["tags"] == [] + + # solutions default + assert cfg.config_dict["solutions"][0]["serviceConfig"]["tags"] == [] + assert cfg.config_dict["solutions"][0]["serviceConfig"]["solutionVersion"]["tags"] == [] + assert cfg.config_dict["solutions"][0]["serviceConfig"]["solutionVersion"]["trainingMode"] == "FULL" + + assert cfg.config_dict["solutions"][1]["serviceConfig"]["tags"] == [] + assert cfg.config_dict["solutions"][1]["serviceConfig"]["solutionVersion"]["tags"] == [] + assert cfg.config_dict["solutions"][1]["serviceConfig"]["solutionVersion"]["trainingMode"] == "FULL" + + # batchSegment defaults + assert cfg.config_dict["solutions"][0]["batchSegmentJobs"][0]["serviceConfig"]["tags"] == [] + + # campaign defaults + assert cfg.config_dict["solutions"][5]["campaigns"][0]["serviceConfig"]["tags"] == [] + + # batchInference defaults + assert cfg.config_dict["solutions"][5]["batchInferenceJobs"][0]["serviceConfig"]["tags"] == [] + + # eventTracker defaults + assert cfg.config_dict["eventTracker"]["serviceConfig"]["tags"] == [] + + # filter defaults + assert cfg.config_dict["filters"][0]["serviceConfig"]["tags"] == [] + + +@mock_sts +def test_dataset_root_tags(root_tags_configuration_path): + """ + Ensures that the root tags are set across all components. + """ + cfg = Configuration() + cfg.load(root_tags_configuration_path) + + validates = cfg.validate() + assert validates + assert len(cfg._configuration_errors) == 0 + + # datasetGroup defaults + assert cfg.config_dict["datasetGroup"]["serviceConfig"]["tags"] == [{"tagKey": "hello", "tagValue": "world"}] + + # dataset-import defaults + assert cfg.config_dict["datasets"]["serviceConfig"]["importMode"] == "FULL" + assert cfg.config_dict["datasets"]["serviceConfig"]["tags"] == [{"tagKey": "hello", "tagValue": "world"}] + + assert cfg.config_dict["datasets"]["serviceConfig"]["publishAttributionMetricsToS3"] == False + + # dataset defaults + assert cfg.config_dict["datasets"]["users"]["dataset"]["serviceConfig"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + assert cfg.config_dict["datasets"]["interactions"]["dataset"]["serviceConfig"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + assert cfg.config_dict["datasets"]["items"]["dataset"]["serviceConfig"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + + # solutions default + assert cfg.config_dict["solutions"][0]["serviceConfig"]["tags"] == [{"tagKey": "hello", "tagValue": "world"}] + assert cfg.config_dict["solutions"][0]["serviceConfig"]["solutionVersion"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + assert cfg.config_dict["solutions"][0]["serviceConfig"]["solutionVersion"]["trainingMode"] == "FULL" + + assert cfg.config_dict["solutions"][1]["serviceConfig"]["tags"] == [{"tagKey": "hello", "tagValue": "world"}] + assert cfg.config_dict["solutions"][1]["serviceConfig"]["solutionVersion"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + assert cfg.config_dict["solutions"][1]["serviceConfig"]["solutionVersion"]["trainingMode"] == "FULL" + + # batchSegment defaults + assert cfg.config_dict["solutions"][0]["batchSegmentJobs"][0]["serviceConfig"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + + # campaign defaults + assert cfg.config_dict["solutions"][1]["campaigns"][0]["serviceConfig"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + + # batchInference defaults + assert cfg.config_dict["solutions"][1]["batchInferenceJobs"][0]["serviceConfig"]["tags"] == [ + {"tagKey": "hello", "tagValue": "world"} + ] + + # eventTracker defaults + assert cfg.config_dict["eventTracker"]["serviceConfig"]["tags"] == [{"tagKey": "hello", "tagValue": "world"}] + + # filter defaults + assert cfg.config_dict["filters"][0]["serviceConfig"]["tags"] == [{"tagKey": "hello", "tagValue": "world"}] + + +@mock_sts +def test_bad_root_tag_keys(): + cfg = Configuration() + config = """ + { + "tags": [{"tagKeys": "tagKey", "tagValue": "tagValue"}], + "datasetGroup": {"serviceConfig": {"name": "testing-tags"}} + } + """ + cfg.load(str(config)) + + validates = cfg.validate() + assert cfg._configuration_errors == ["Parameter validation failed: Tag keys must be one of: 'tagKey', 'tagValue'"] + assert validates == False + + +@mock_sts +def test_bad_tag_keys(): + cfg = Configuration() + config = """{ + "datasetGroup": { + "serviceConfig": {"name": "testing-tags", "tags": [{"tagKeys": "tagKey", "tagValue": "tagValue"}]} + } + } + """ + + cfg.load(str(config)) + validates = cfg.validate() + + assert cfg._configuration_errors == [ + 'Parameter validation failed: Missing required parameter in tags[0]: "tagKey" Unknown parameter in tags[0]: "tagKeys", must be one of: tagKey, tagValue' + ] + assert validates == False + + +@mock_sts +def test_more_bad_root_tag_keys(): + cfg = Configuration() + config = """ + { + "tags": {}, + "datasetGroup": {"serviceConfig": {"name": "testing-tags"}} + } + """ + cfg.load(str(config)) + validates = cfg.validate() + + assert cfg._configuration_errors == ["Invalid type at path root for tags, expected list[dict]."] + assert validates == False + + +@mock_sts +def test_more_bad_tag_keys(): + cfg = Configuration() + config = """ + { + + "datasetGroup": {"serviceConfig": {"name": "testing-tags", "tags": {}}} + } + """ + cfg.load(str(config)) + + validates = cfg.validate() + print(cfg._configuration_errors) + + assert cfg._configuration_errors == [ + "Parameter validation failed: Invalid type for parameter tags, value: {}, type: , valid types: , " + ] + assert validates == False + + +@mock_sts +def test_root_tag_keys(): + cfg = Configuration() + config = """ + { + "tags": [{"tagKey": "tagKey", "tagValue": "tagValue"}], + "datasetGroup": {"serviceConfig": {"name": "testing-tags"}} + } + """ + cfg.load(str(config)) + + validates = cfg.validate() + + assert cfg._configuration_errors == [] + assert validates + + +@mock_sts +def test_tag_keys(): + cfg = Configuration() + config = """{ + "datasetGroup": { + "serviceConfig": {"name": "testing-tags", "tags": [{"tagKey": "tagKey", "tagValue": "tagValue"}]} + } + } + """ + cfg.load(str(config)) + + validates = cfg.validate() + + assert cfg._configuration_errors == [] + assert validates + + +@mock_sts +def test_dataset_group_args(tags_configuration_path, monkeypatch, argtest): + """ + Ensuring params to validation calls are as expected per the config supplied. + """ + cfg = Configuration() + cfg.load(tags_configuration_path) + + # returns arguments passed to mocked calls + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + validates = cfg._validate_dataset_group() + assert validates is None + assert len(cfg._configuration_errors) == 0 + assert argtest.args[1] == {"name": "unit_test_new_datasetgroup", "tags": [{"tagKey": "tag0", "tagValue": "key0"}]} + + +@mock_sts +def test_dataset_args(tags_configuration_path, monkeypatch, argtest): + cfg = Configuration() + cfg.load(tags_configuration_path) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + cfg._validate_datasets() + assert len(cfg._configuration_errors) == 0 + assert argtest.args[1] == { + "name": "unit_test_only_interactions", + "tags": [{"tagKey": "tag3", "tagValue": "key3"}], + "datasetGroupArn": f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:dataset-group/validation", + "schemaArn": f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:schema/validation", + "datasetType": "interactions", + } + + +@mock_sts +def test_dataset_import_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "datasets": { + "serviceConfig": { + "name": "dataset_import_config", + "importMode": "FULL", + "tags": [{"tagKey": "1", "tagValue": "1"}] + } + } + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + cfg._validate_dataset_import_job() + assert len(cfg._configuration_errors) == 0 + assert argtest.args[1] == { + "name": "dataset_import_config", + "importMode": "FULL", + "tags": [{"tagKey": "1", "tagValue": "1"}], + } + + +@mock_sts +def test_solution_version_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "solutions": [ + { + "serviceConfig": { + "name": "unit_test_new_solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity", + "solutionVersion": { + "trainingMode": "FULL", + "tags": [{"tagKey": "1", "tagValue": "2"}] + } + } + } + ] + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + cfg._validate_solution_version(cfg.config_dict["solutions"][0]["serviceConfig"]) + assert len(cfg._configuration_errors) == 0 + assert argtest.args[1] == {"trainingMode": "FULL", "tags": [{"tagKey": "1", "tagValue": "2"}]} + + +@mock_sts +def test_solution_version_unsupported_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "solutions": [ + { + "serviceConfig": { + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity", + "solutionVersion": { + "name": "SolutionV1", + "tags": [{"tagKey": "1", "tagValue": "2"}] + } + } + } + ] + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + cfg._validate_solution_version(cfg.config_dict["solutions"][0]["serviceConfig"]) + assert argtest.args[1] == {"name": "SolutionV1", "tags": [{"tagKey": "1", "tagValue": "2"}]} + assert cfg._configuration_errors == [ + "Allowed keys for solutionVersion are: ['trainingMode', 'tags']. Unsupported key(s): ['name']" + ] + + +@mock_sts +def test_batch_inference_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "solutions": [ + { + "serviceConfig": { + "name": "unit_test_new_solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity" + }, + "batchInferenceJobs": [{"serviceConfig": { + "tags": [{"tagKey": "tag1", "tagValue": "key1"}] + }}] + } + ] + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + solution = cfg.config_dict["solutions"][0] + + cfg._validate_batch_inference_jobs( + "solutions[0].batchInferenceJobs", + solution["serviceConfig"]["name"], + solution["batchInferenceJobs"], + ) + assert cfg._configuration_errors == [] + + args = argtest.args[1] + assert args["solutionVersionArn"] == f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:solution/validation/unknown" + assert args["jobName"].startswith("batch_" + solution["serviceConfig"]["name"]) + assert args["roleArn"] == "roleArn" + assert args["jobInput"] == {"s3DataSource": {"path": "s3://data-source"}} + assert args["jobOutput"] == {"s3DataDestination": {"path": "s3://data-destination"}} + assert args["tags"] == [{"tagKey": "tag1", "tagValue": "key1"}] + + +@mock_sts +def test_campaign_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "solutions": [ + { + "serviceConfig": { + "name": "unit_test_new_solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity" + }, + "campaigns": [{"serviceConfig": {"name": "campaign1", "tags": [{"tagKey": "tag1", "tagValue": "key1"}]}}] + } + ] + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + solution = cfg.config_dict["solutions"][0] + + cfg._validate_campaigns(f"solutions[0].campaigns", solution["campaigns"]) + assert cfg._configuration_errors == [] + assert argtest.args[1] == { + "name": "campaign1", + "tags": [{"tagKey": "tag1", "tagValue": "key1"}], + "solutionVersionArn": f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:solution/validation/unknown", + } + + +@mock_sts +def test_batch_segment_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "solutions": [ + { + "serviceConfig": { + "name": "unit_test_new_solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity" + }, + "batchSegmentJobs": [{"serviceConfig": { + "tags": [{"tagKey": "tag1", "tagValue": "key1"}] + }}] + } + ] + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + solution = cfg.config_dict["solutions"][0] + + cfg._validate_batch_inference_jobs( + "solutions[0].batchSegmentJobs", + solution["serviceConfig"]["name"], + solution["batchSegmentJobs"], + ) + assert cfg._configuration_errors == [] + + args = argtest.args[1] + assert args["solutionVersionArn"] == f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:solution/validation/unknown" + assert args["jobName"].startswith("batch_" + solution["serviceConfig"]["name"]) + assert args["roleArn"] == "roleArn" + assert args["jobInput"] == {"s3DataSource": {"path": "s3://data-source"}} + assert args["jobOutput"] == {"s3DataDestination": {"path": "s3://data-destination"}} + assert args["tags"] == [{"tagKey": "tag1", "tagValue": "key1"}] + + +@mock_sts +def test_batch_inference_args(monkeypatch, argtest): + cfg = Configuration() + cfg.load( + """ + { + "datasetGroup": {"serviceConfig": {"name": "unit_test_new_datasetgroup"}}, + "solutions": [ + { + "serviceConfig": { + "name": "unit_test_new_solution", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity" + }, + "batchInferenceJobs": [{"serviceConfig": {"tags": [{"tagKey": "tag1", "tagValue": "key1"}]}}] + } + ] + } + """ + ) + + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + solution = cfg.config_dict["solutions"][0] + + cfg._validate_batch_inference_jobs( + "solutions[0].batchInferenceJobs", + solution["serviceConfig"]["name"], + solution["batchInferenceJobs"], + ) + assert cfg._configuration_errors == [] + + args = argtest.args[1] + assert args["solutionVersionArn"] == f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:solution/validation/unknown" + assert args["jobName"].startswith("batch_" + solution["serviceConfig"]["name"]) + assert args["roleArn"] == "roleArn" + assert args["jobInput"] == {"s3DataSource": {"path": "s3://data-source"}} + assert args["jobOutput"] == {"s3DataDestination": {"path": "s3://data-destination"}} + assert args["tags"] == [{"tagKey": "tag1", "tagValue": "key1"}] + + +def test_recommender_args(tags_configuration_path, monkeypatch, argtest): + cfg = Configuration() + cfg.load(tags_configuration_path) + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + cfg._validate_recommender() + assert len(cfg._configuration_errors) == 0 + + assert argtest.args[1] == { + "name": "ddsg-most-viewed", + "recipeArn": "arn:aws:personalize:::recipe/aws-ecomm-popular-items-by-views", + "tags": [{"tagKey": "hello13", "tagValue": "world13"}], + } + + +@mock_sts +def test_filter_args(tags_configuration_path, monkeypatch, argtest): + cfg = Configuration() + cfg.load(tags_configuration_path) + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + cfg._validate_filters() + assert len(cfg._configuration_errors) == 0 + + assert argtest.args[1] == { + "name": "clicked-or-streamed-2", + "filterExpression": 'INCLUDE ItemID WHERE Interactions.EVENT_TYPE in ("click", "stream")', + "tags": [{"tagKey": "tag11", "tagValue": "key11"}], + "datasetGroupArn": f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:dataset-group/validation", + } + + +@mock_sts +def test_event_tracker_args(tags_configuration_path, monkeypatch, argtest): + cfg = Configuration() + cfg.load(tags_configuration_path) + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + cfg._validate_event_tracker() + assert len(cfg._configuration_errors) == 0 + + assert argtest.args[1] == { + "name": "unit_test_new_event_tracker", + "tags": [{"tagKey": "tag10", "tagValue": "key10"}], + "datasetGroupArn": f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:dataset-group/validation", + } + + +@mock_sts +def test_event_tracker_args(tags_configuration_path, monkeypatch, argtest): + cfg = Configuration() + cfg.load(tags_configuration_path) + monkeypatch.setattr("aws_lambda.shared.personalize_service.Configuration._fill_default_vals", argtest) + + cfg._validate_event_tracker() + assert len(cfg._configuration_errors) == 0 + + assert argtest.args[1] == { + "name": "unit_test_new_event_tracker", + "tags": [{"tagKey": "tag10", "tagValue": "key10"}], + "datasetGroupArn": f"arn:aws:personalize:us-east-1:{ACCOUNT_ID}:dataset-group/validation", + } diff --git a/source/tests/aws_lambda/test_sfn_middleware.py b/source/tests/aws_lambda/test_sfn_middleware.py index 51c8419..e474c3a 100644 --- a/source/tests/aws_lambda/test_sfn_middleware.py +++ b/source/tests/aws_lambda/test_sfn_middleware.py @@ -15,22 +15,21 @@ from decimal import Decimal import pytest -from moto import mock_sts - from aws_lambda.shared.sfn_middleware import ( - PersonalizeResource, - STATUS_IN_PROGRESS, STATUS_FAILED, - ResourcePending, + STATUS_IN_PROGRESS, + Parameter, + PersonalizeResource, ResourceFailed, ResourceInvalid, + ResourcePending, json_handler, - set_defaults, - set_bucket, parse_datetime, - Parameter, + set_bucket, + set_defaults, set_workflow_config, ) +from moto import mock_sts from shared.resource import DatasetGroup @@ -70,7 +69,7 @@ def test_personalize_resource_decorator(personalize_resource, personalize_stubbe """ The typical workflow is to describe, then create, then raise ResourcePending """ - dsg_name = "dsgName" + dsg_name = "mockDatasetGroup" personalize_stubber.add_client_error("describe_dataset_group", "ResourceNotFoundException") personalize_stubber.add_response( "create_dataset_group", @@ -204,6 +203,7 @@ def test_parameter_resolution(key, source, path, format_as, default, result): def test_set_workflow_config(): result = set_workflow_config( { + "tags": [{"tagKey": "tag1", "tagValue": "key1"}], "datasetGroup": { "serviceConfig": {"datasetGroup": "should-not-change"}, "workflowConfig": {"maxAge": "one day"}, @@ -212,6 +212,7 @@ def test_set_workflow_config(): "serviceConfig": {}, }, "datasets": { + "serviceConfig": {}, "users": { "dataset": {"serviceConfig": {}}, "schema": {"serviceConfig": {}}, @@ -228,7 +229,14 @@ def test_set_workflow_config(): "filters": [{"serviceConfig": {}}], "solutions": [ { - "serviceConfig": {"datasetGroup": "should-not-change"}, + "serviceConfig": { + "datasetGroup": "should-not-change", + "tags": [{"tagKey": "mockSolution", "tagValue": "solutionKey"}], + "solutionVersion": { + "name": "mockSolutionVersion", + "tags": [{"tagKey": "mockSolutionVersion", "tagValue": "solutionVersionKey"}], + }, + }, "campaigns": [ { "serviceConfig": {}, @@ -257,6 +265,13 @@ def test_set_workflow_config(): # keys under serviceConfig should not change assert result.get("datasetGroup").get("serviceConfig").get("datasetGroup") == "should-not-change" assert result.get("solutions")[0].get("serviceConfig").get("datasetGroup") == "should-not-change" + assert result.get("solutions")[0].get("serviceConfig").get("tags") == [ + {"tagKey": "mockSolution", "tagValue": "solutionKey"} + ] + assert result.get("solutions")[0].get("serviceConfig").get("solutionVersion").get("tags") == [ + {"tagKey": "mockSolutionVersion", "tagValue": "solutionVersionKey"} + ] # overrides to the default must remain unchanged assert result.get("solutions")[0]["campaigns"][0]["workflowConfig"]["maxAge"] == "should-not-change" + assert result.get("solutions")[0]["campaigns"][0]["workflowConfig"]["maxAge"] == "should-not-change" diff --git a/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml b/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml index 96b05d7..2c1c716 100644 --- a/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml +++ b/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = ["AWS Solutions Builders"] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.9" minimal = {path = "package"} [tool.poetry.dev-dependencies] diff --git a/source/tests/cdk_solution_helper/aws_lambda/python/test_function.py b/source/tests/cdk_solution_helper/aws_lambda/python/test_function.py index 88c8f94..098a86d 100644 --- a/source/tests/cdk_solution_helper/aws_lambda/python/test_function.py +++ b/source/tests/cdk_solution_helper/aws_lambda/python/test_function.py @@ -77,7 +77,7 @@ def test_function_has_default_role(function_synth): func = function_stack["Resources"]["TestFunction"] assert func["Type"] == "AWS::Lambda::Function" assert func["Properties"]["Handler"] == PYTHON_FUNCTION_NAME.split(".")[0] + "." + PYTHON_FUNCTION_HANDLER_NAME - assert func["Properties"]["Runtime"] == "python3.7" + assert func["Properties"]["Runtime"] == "python3.9" role = function_stack["Resources"][func["Properties"]["Role"]["Fn::GetAtt"][0]] assert role["Type"] == "AWS::IAM::Role" diff --git a/source/tests/conftest.py b/source/tests/conftest.py index 07c344d..f6d4cde 100644 --- a/source/tests/conftest.py +++ b/source/tests/conftest.py @@ -10,9 +10,9 @@ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for # # the specific language governing permissions and limitations under the License. # # ###################################################################################################################### +import json import os import sys -import json from pathlib import Path from tempfile import TemporaryDirectory from typing import Dict, Optional @@ -21,18 +21,17 @@ import jsii import pytest from aws_cdk.aws_lambda import ( - FunctionProps, Code, - Runtime, Function, - LayerVersionProps, + FunctionProps, LayerVersion, + LayerVersionProps, + Runtime, ) +from aws_solutions.core import get_service_client from botocore.stub import Stubber from constructs import Construct -from aws_solutions.core import get_service_client - shared_path = str(Path(__file__).parent.parent / "aws_lambda") if shared_path not in sys.path: sys.path.insert(0, shared_path) @@ -99,7 +98,7 @@ def mock_lambda_init( props = FunctionProps( code=Code.from_inline("return"), handler=handler, - runtime=Runtime.PYTHON_3_7, + runtime=Runtime.PYTHON_3_9, **kwargs, ) jsii.create(Function, self, [scope, id, props]) @@ -110,7 +109,7 @@ def mock_layer_init(self, scope: Construct, id: str, *, code: Code, **kwargs) -> # override the runtime list for now, as well, to match above with TemporaryDirectory() as tmpdirname: kwargs["code"] = Code.from_asset(path=tmpdirname) - kwargs["compatible_runtimes"] = [Runtime.PYTHON_3_7] + kwargs["compatible_runtimes"] = [Runtime.PYTHON_3_9] props = LayerVersionProps(**kwargs) jsii.create(LayerVersion, self, [scope, id, props]) @@ -131,6 +130,25 @@ def configuration_path(): return Path(__file__).parent / "fixtures" / "config" / "sample_config.json" +@pytest.fixture +def tags_configuration_path(): + return Path(__file__).parent / "fixtures" / "config" / "sample_config_wtags.json" + + +@pytest.fixture +def root_tags_configuration_path(): + return Path(__file__).parent / "fixtures" / "config" / "sample_config_root_tags.json" + + +@pytest.fixture +def argtest(): + class TestArgs(object): + def __call__(self, *args): + self.args = list(args) + + return TestArgs() + + class NotifierStub(Notifier): def __init__(self): self.creation_notifications = [] @@ -197,9 +215,12 @@ def _validate_handler_config(resource: str, config: Dict, status: Optional[str] shape = resource[0].upper() + resource[1:] request_shape = cli.meta.service_model.shape_for(f"Create{shape}Request") - del request_shape.members["tags"] - if "importMode" in request_shape.members: - del request_shape.members["importMode"] + + if "performAutoML" in request_shape.members: + del request_shape.members["performAutoML"] + + if shape == "SolutionVersion": + del request_shape.members["name"] response_shape = cli.meta.service_model.shape_for(f"Describe{shape}Response") diff --git a/source/tests/fixtures/config/sample_config.json b/source/tests/fixtures/config/sample_config.json index 626f0ee..5b9e4fd 100644 --- a/source/tests/fixtures/config/sample_config.json +++ b/source/tests/fixtures/config/sample_config.json @@ -80,6 +80,35 @@ } } } + }, + "items": { + "dataset": { + "serviceConfig": { + "name": "items-dataset" + } + }, + "schema": { + "serviceConfig": { + "name": "items-schema", + "schema": { + "type": "record", + "name": "Items", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "GENRES", + "type": "string", + "categorical": true + } + ], + "version": "1.0" + } + } + } } }, "solutions": [ @@ -89,12 +118,12 @@ "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity" }, "batchSegmentJobs": [ - { - "serviceConfig": {}, - "workflowConfig": { - "schedule": "cron(0 3 * * ? *)" - } + { + "serviceConfig": {}, + "workflowConfig": { + "schedule": "cron(0 3 * * ? *)" } + } ] }, { @@ -106,7 +135,7 @@ { "serviceConfig": {}, "workflowConfig": { - "schedule": "cron(0 3 * * ? *)" + "schedule": "cron(0 3 * * ? *)" } } ] diff --git a/source/tests/fixtures/config/sample_config_root_tags.json b/source/tests/fixtures/config/sample_config_root_tags.json new file mode 100644 index 0000000..f1df692 --- /dev/null +++ b/source/tests/fixtures/config/sample_config_root_tags.json @@ -0,0 +1,149 @@ +{ + "tags": [ + { + "tagKey": "hello", + "tagValue": "world" + } + ], + "datasetGroup": { + "serviceConfig": { + "name": "testing-tags" + } + }, + "datasets": { + "interactions": { + "dataset": { + "serviceConfig": { + "name": "interactions-dataset" + } + }, + "schema": { + "serviceConfig": { + "name": "interactions-schema", + "schema": { + "type": "record", + "name": "Interactions", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "EVENT_TYPE", + "type": "string" + } + ], + "version": "1.0" + } + } + } + }, + "items": { + "dataset": { + "serviceConfig": { + "name": "items-dataset" + } + }, + "schema": { + "serviceConfig": { + "name": "items-schema", + "schema": { + "type": "record", + "name": "Items", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "GENRES", + "type": "string", + "categorical": true + } + ], + "version": "1.0" + } + } + } + }, + "users": { + "dataset": { + "serviceConfig": { + "name": "users-dataset" + } + }, + "schema": { + "serviceConfig": { + "name": "users-schema", + "schema": { + "type": "record", + "name": "Users", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "GENDER", + "type": "string", + "categorical": true + } + ], + "version": "1.0" + } + } + } + } + }, + "solutions": [ + { + "serviceConfig": { + "name": "affinity_item" + }, + "batchSegmentJobs": [ + { + "serviceConfig": {} + } + ] + }, + { + "serviceConfig": { + "name": "unit_test_personalized_ranking_new_2", + "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization" + }, + "campaigns": [ + { + "serviceConfig": { + "name": "personalized_ranking_campaign", + "minProvisionedTPS": 1 + } + } + ], + "batchInferenceJobs": [ + { + "serviceConfig": {} + } + ] + } + ], + "eventTracker": { + "serviceConfig": { + "name": "unit_test_new_event_tracker" + } + }, + "filters": [ + { + "serviceConfig": { + "name": "clicked-or-streamed-2", + "filterExpression": "INCLUDE ItemID WHERE Interactions.EVENT_TYPE in ('click', 'stream')" + } + } + ] +} \ No newline at end of file diff --git a/source/tests/fixtures/config/sample_config_wtags.json b/source/tests/fixtures/config/sample_config_wtags.json new file mode 100644 index 0000000..c33d9a5 --- /dev/null +++ b/source/tests/fixtures/config/sample_config_wtags.json @@ -0,0 +1,324 @@ +{ + "datasetGroup": { + "serviceConfig": { + "name": "unit_test_new_datasetgroup", + "tags": [ + { + "tagKey": "tag0", + "tagValue": "key0" + } + ] + }, + "workflowConfig": { + "schedules": { + "import": "cron(0 */6 * * ? *)" + } + } + }, + "datasets": { + "serviceConfig": { + "importMode": "FULL", + "tags": [ + { + "tagKey": "tag1", + "tagValue": "key1" + } + ] + }, + "users": { + "dataset": { + "serviceConfig": { + "name": "unit_test_only_users", + "tags": [ + { + "tagKey": "tag2", + "tagValue": "key2" + } + ] + } + }, + "schema": { + "serviceConfig": { + "name": "unit_test_only_users_schema", + "schema": { + "type": "record", + "name": "users", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "AGE", + "type": "int" + }, + { + "name": "GENDER", + "type": "string", + "categorical": true + } + ] + } + } + } + }, + "items": { + "dataset": { + "serviceConfig": { + "name": "items-dataset" + } + }, + "schema": { + "serviceConfig": { + "name": "items-schema", + "schema": { + "type": "record", + "name": "Items", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "GENRES", + "type": "string", + "categorical": true + } + ], + "version": "1.0" + } + } + } + }, + "interactions": { + "dataset": { + "serviceConfig": { + "name": "unit_test_only_interactions", + "tags": [ + { + "tagKey": "tag3", + "tagValue": "key3" + } + ] + } + }, + "schema": { + "serviceConfig": { + "name": "unit_test_only_interactions_schema", + "schema": { + "type": "record", + "name": "interactions", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + { + "name": "ITEM_ID", + "type": "string" + }, + { + "name": "USER_ID", + "type": "string" + }, + { + "name": "TIMESTAMP", + "type": "long" + }, + { + "name": "EVENT_TYPE", + "type": "string" + }, + { + "name": "EVENT_VALUE", + "type": "float" + } + ] + } + } + } + } + }, + "solutions": [ + { + "serviceConfig": { + "name": "affinity_item", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-affinity", + "tags": [ + { + "tagKey": "tag4", + "tagValue": "key4" + } + ], + "solutionVersion": { + "tags": [ + { + "tagKey": "tag5", + "tagValue": "key5" + } + ] + } + }, + "batchSegmentJobs": [ + { + "serviceConfig": { + "tags": [ + { + "tagKey": "tag6", + "tagValue": "key6" + } + ] + }, + "workflowConfig": { + "schedule": "cron(0 3 * * ? *)" + } + } + ] + }, + { + "serviceConfig": { + "name": "affinity_item_attribute", + "recipeArn": "arn:aws:personalize:::recipe/aws-item-attribute-affinity", + "tags": [ + { + "tagKey": "tag7", + "tagValue": "key7" + } + ] + }, + "batchSegmentJobs": [ + { + "serviceConfig": {}, + "workflowConfig": { + "schedule": "cron(0 3 * * ? *)" + } + } + ] + }, + { + "serviceConfig": { + "name": "unit_test_sims_new", + "recipeArn": "arn:aws:personalize:::recipe/aws-sims" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 0 ? * 1 *)" + } + } + }, + { + "serviceConfig": { + "name": "unit_test_popularity_count_new", + "recipeArn": "arn:aws:personalize:::recipe/aws-popularity-count" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 1 ? * 1 *)" + } + } + }, + { + "serviceConfig": { + "name": "unit_test_personalized_ranking_new", + "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 2 ? * 1 *)" + } + }, + "campaigns": [ + { + "serviceConfig": { + "name": "unit_test_personalized_ranking_new_campaign", + "minProvisionedTPS": 1, + "tags": [ + { + "tagKey": "tag8", + "tagValue": "key8" + } + ] + } + } + ] + }, + { + "serviceConfig": { + "name": "unit_test_personalized_ranking_new_2", + "recipeArn": "arn:aws:personalize:::recipe/aws-user-personalization" + }, + "workflowConfig": { + "schedules": { + "full": "cron(0 2 ? * 1 *)" + } + }, + "campaigns": [ + { + "serviceConfig": { + "name": "unit_test_personalized_ranking_2_campaign", + "minProvisionedTPS": 1 + } + } + ], + "batchInferenceJobs": [ + { + "serviceConfig": { + "tags": [ + { + "tagKey": "tag9", + "tagValue": "key9" + } + ] + }, + "workflowConfig": { + "schedule": "cron(0 3 * * ? *)" + } + } + ] + } + ], + "eventTracker": { + "serviceConfig": { + "name": "unit_test_new_event_tracker", + "tags": [ + { + "tagKey": "tag10", + "tagValue": "key10" + } + ] + } + }, + "filters": [ + { + "serviceConfig": { + "name": "clicked-or-streamed-2", + "filterExpression": "INCLUDE ItemID WHERE Interactions.EVENT_TYPE in (\"click\", \"stream\")", + "tags": [ + { + "tagKey": "tag11", + "tagValue": "key11" + } + ] + } + } + ], + "tags": [ + { + "tagKey": "tag12", + "tagValue": "key12" + } + ], + "recommenders": [ + { + "serviceConfig": { + "name": "ddsg-most-viewed", + "recipeArn": "arn:aws:personalize:::recipe/aws-ecomm-popular-items-by-views", + "tags": [ + { + "tagKey": "hello13", + "tagValue": "world13" + } + ] + } + } + ] +} \ No newline at end of file