From bf79e31b662212833c04b68a2802c630907c84c6 Mon Sep 17 00:00:00 2001 From: Alexis Lucattini Date: Sat, 9 Mar 2024 17:42:37 +1100 Subject: [PATCH] Added feature icav2 copy batch state machine --- .../icav2_copy_batch_utility/.gitignore | 8 + .../icav2_copy_batch_utility/.npmignore | 6 + .../icav2_copy_batch_utility/Readme.md | 145 ++++ .../icav2_copy_batch_utility/deploy/README.md | 14 + .../deploy/bin/icav2_copy_batch_utility.ts | 15 + .../icav2_copy_batch_utility/deploy/cdk.json | 66 ++ .../deploy/constants.ts | 3 + .../deploy/jest.config.js | 8 + .../constructs/icav2_copy_batch_utility.ts | 158 ++++ .../deploy/lib/constructs/lambda_layer.ts | 45 ++ .../stacks/icav2_copy_batch_utility_stack.ts | 61 ++ .../deploy/package.json | 27 + .../test/icav2_copy_batch_utility.test.ts | 17 + .../deploy/tsconfig.json | 31 + .../images/step_functions_example.png | Bin 0 -> 67595 bytes .../copy_batch_data_handler/handler.py | 67 ++ .../lambdas/job_status_handler/handler.py | 330 ++++++++ .../lambdas/manifest_handler/handler.py | 218 +++++ .../layers/poetry.lock | 760 ++++++++++++++++++ .../layers/pyproject.toml | 32 + .../__init__.py | 0 .../utils/__init__.py | 0 .../utils/aws_ssm_helpers.py | 63 ++ .../utils/compression_helpers.py | 44 + .../utils/globals.py | 13 + .../utils/job_helpers.py | 34 + .../copy_batch_state_machine.json | 188 +++++ 27 files changed, 2353 insertions(+) create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/.gitignore create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/.npmignore create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/Readme.md create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/README.md create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/bin/icav2_copy_batch_utility.ts create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/cdk.json create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/constants.ts create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/jest.config.js create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/icav2_copy_batch_utility.ts create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/lambda_layer.ts create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/stacks/icav2_copy_batch_utility_stack.ts create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/package.json create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/test/icav2_copy_batch_utility.test.ts create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/deploy/tsconfig.json create mode 100755 lib/workload/stateless/icav2_copy_batch_utility/images/step_functions_example.png create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/lambdas/copy_batch_data_handler/handler.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/lambdas/job_status_handler/handler.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/lambdas/manifest_handler/handler.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/poetry.lock create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/pyproject.toml create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/__init__.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/__init__.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/aws_ssm_helpers.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/compression_helpers.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/globals.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/job_helpers.py create mode 100644 lib/workload/stateless/icav2_copy_batch_utility/step_functions_templates/copy_batch_state_machine.json diff --git a/lib/workload/stateless/icav2_copy_batch_utility/.gitignore b/lib/workload/stateless/icav2_copy_batch_utility/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lib/workload/stateless/icav2_copy_batch_utility/.npmignore b/lib/workload/stateless/icav2_copy_batch_utility/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lib/workload/stateless/icav2_copy_batch_utility/Readme.md b/lib/workload/stateless/icav2_copy_batch_utility/Readme.md new file mode 100644 index 000000000..ca8791806 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/Readme.md @@ -0,0 +1,145 @@ +# ICAv2 Copy Batch Utility + +## Overview + +The icav2 copy batch utility CDK wraps an AWS Step Function over the ICAv2 CopyBatch API. +This api is designed to copy a list of illumina file ids into a directory. + +We exploit this API by taking in a manifest (a list of key values of source ids with their respective destinations) ( +a source may have multiple destinations if it needs to be copied into a few different places +), and then monitor a set of API jobs to completion. + +These CopyBatch API jobs have about a 20% fail rate, so we sometimes need to resubmit, this is built into the step function. + +A 20% fail rate seems quite high, but when they work, these jobs can transfer 80 Gb of data in under 10 seconds so it's worth +persisting with. + +When working with the ICAv2 CopyBatch API, we need to provide a unique identifier for the run, and a unique location for the outputs. + +Once all jobs are deployed we monitor their status (redeploy if failed - up to five times) and wait for all jobs to complete. + +This process will transfer an entire BCLConvert process from one ICAv2 project to another in under 10 minutes! + +Note that a current limitation prevents using this API within the same project. + + +## Inputs + +* Statemachine expects the following inputs: + * One of `manifest` or `manifest_b64gz`. + + +An example is below + +```json +{ + "manifest": { + "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R1_001.fastq.gz": [ + "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_cttso_fastq_cache/20240308abcd1234/L2301368_run_cache/L2301368/" + ], + "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R2_001.fastq.gz": [ + "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_cttso_fastq_cache/20240308abcd1234/L2301368_run_cache/L2301368/" + ] + } +} +``` + +OR + +```json +{ + "manifest_b64gz": "H4sIAAAAAAAAA+2SzUoDMRhF932KoWvT/P9111ZFod0oqCASMklaBqYzY5IOFvHdHRXBlYtutND9x/3uOdzXUVGMK2d7MoWwJHRdciyAV5wAphUH1jsHFMUOk6CcJwRW9bYBtrH1PoUECcUYCzNDGHFiEJbEzK/ulit+fvsgjSdMcGyY08g7MHf1om36EHPRM0OMBKXE2imvAQ/CDw+DAFaVATiLZcmD9gyVsN3lbpfhst0keGlTfl60264OOUzySx5Pi8cB4QeE5JoHtSaAEk8Bc3LIpIQBJ6zi3gY1JH9CmC5WWxv3kCBCfweBVAgsuP44ZYgiaUvnpdLoq9V4aPB0dlwm721sqmaTJnW7OUk8UOJFjG08KTxQ4U3o2pgTnHnb5RDNKuRYuTRxqf9rnd/Vjszo5wqum3X7vyY5ehu9Ax+WAaFoBgAA" +} +``` + +## Outputs + +```json +{ + "job_status_iterable_parameter": { + "job_status_iterable": 1 + }, + "wait_parameter": { + "wait": true + }, + "counters": { + "jobs_failed": 0, + "jobs_running": 0, + "jobs_passed": 1 + }, + "job_list_with_attempt_counter": [ + { + "job_attempt_counter": 1, + "job_id": "6f3d6981-0dff-4413-8388-2bb445d03dd7", + "failed_jobs_list": [], + "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_cttso_fastq_cache/20240308abcd1234/L2301368_run_cache/L2301368/", + "source_uris": [ + "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R1_001.fastq.gz", + "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R2_001.fastq.gz" + ], + "job_status": true + } + ] +} +``` + +## Lambdas in this directory + +All lambdas run on python 3.11 or higher. + +### Flip Manifest + +This lambda takes in a manifest and flips the keys and values. +The dictionary becomes a list of objects where the keys are values under `dest_uri` and values are under +the list `source_uris` + +In the example above, because both files are heading to the same directory we get the following output + +```json +[ + { + "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_cttso_fastq_cache/20240308abcd1234/L2301368_run_cache/L2301368/", + "source_uris": [ + "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R1_001.fastq.gz", + "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R2_001.fastq.gz" + ] + } +] +``` + +### Launch Copy Job + +Simple function that takes in a dest uri and a list of source uris, converts both into a folder id and file ids respectively +and launches the ICAv2 Copy Data Batch Job. + +This returns a job id, we tie the job id with the dest uri and source uris (because we cannot collect these from the job themselves), +and monitor the job throughout the step function process. + +This lambda is called in a map state. + + +### Update job session + +Goes through the list of jobs (breaks after 10 to prevent timeouts) and + +1. Checks the current job id for the dest uri and source uris combination +2. If the job has failed, resubmits it and increments the attempt counter, also adds the previous job id to the failed jobs list +3. If the job has passed, move the job id to the front, so we don't need to monitor it again. + + +## SSM Parameters + +### Parameters generated by CDK + +``` + +``` + +### External Parameters required by CDK + +``` +"/icav2/umccr-prod/tso500_ctdna_2.1_pipeline_id" +"/icav2_copy_batch_utility/state_machine_arn" +``` + + diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/README.md b/lib/workload/stateless/icav2_copy_batch_utility/deploy/README.md new file mode 100644 index 000000000..9315fe5b9 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `npx cdk deploy` deploy this stack to your default AWS account/region +* `npx cdk diff` compare deployed stack with current state +* `npx cdk synth` emits the synthesized CloudFormation template diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/bin/icav2_copy_batch_utility.ts b/lib/workload/stateless/icav2_copy_batch_utility/deploy/bin/icav2_copy_batch_utility.ts new file mode 100644 index 000000000..7b1214b59 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/bin/icav2_copy_batch_utility.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { ICAv2CopyBatchUtilityStack } from '../lib/stacks/icav2_copy_batch_utility_stack'; +import { ICAV2_JWT_SECRET_ARN_SSM_PARAMETER_PATH, ICAV2_COPY_BATCH_STATE_MACHINE_ARN_SSM_PARAMETER_PATH } from '../constants'; + +const app = new cdk.App(); +new ICAv2CopyBatchUtilityStack(app, 'Icav2CopyBatchUtilityStack', { + icav2_jwt_ssm_parameter_path: ICAV2_JWT_SECRET_ARN_SSM_PARAMETER_PATH, + icav2_copy_batch_state_machine_ssm_parameter_path: ICAV2_COPY_BATCH_STATE_MACHINE_ARN_SSM_PARAMETER_PATH, + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION + }, +}); \ No newline at end of file diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/cdk.json b/lib/workload/stateless/icav2_copy_batch_utility/deploy/cdk.json new file mode 100644 index 000000000..a88cbf408 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/cdk.json @@ -0,0 +1,66 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/icav2_copy_batch_utility.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true + } +} diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/constants.ts b/lib/workload/stateless/icav2_copy_batch_utility/deploy/constants.ts new file mode 100644 index 000000000..10dbff109 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/constants.ts @@ -0,0 +1,3 @@ +export const ICAV2_JWT_SECRET_ARN_SSM_PARAMETER_PATH = "/icav2/umccr-prod/service-user-trial-jwt-token-secret-arn" + +export const ICAV2_COPY_BATCH_STATE_MACHINE_ARN_SSM_PARAMETER_PATH= "/icav2_copy_batch_utility/state_machine_arn" diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/jest.config.js b/lib/workload/stateless/icav2_copy_batch_utility/deploy/jest.config.js new file mode 100644 index 000000000..08263b895 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/icav2_copy_batch_utility.ts b/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/icav2_copy_batch_utility.ts new file mode 100644 index 000000000..b5f049c50 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/icav2_copy_batch_utility.ts @@ -0,0 +1,158 @@ +import { Duration } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { DefinitionBody } from 'aws-cdk-lib/aws-stepfunctions'; + +import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; +import { LambdaLayerConstruct } from './lambda_layer'; + + +export interface ICAv2CopyBatchUtilityConstructProps { + icav2_jwt_ssm_parameter_path: string; // "/icav2/umccr-prod/service-production-jwt-token-secret-arn" + lambdas_layer_path: string; // __dirname + '/../../../layers + manifest_handler_lambda_path: string; // __dirname + '/../../../lambdas/manifest_handler' + copy_batch_data_lambda_path: string; // __dirname + '/../../../lambdas/copy_batch_data_handler' + job_status_handler_lambda_path: string; // __dirname + '/../../../lambdas/job_status_handler' + definition_body_path: string; // __dirname + '/../../../step_functions_templates/copy_batch_state_machine.json' +} + +export class ICAv2CopyBatchUtilityConstruct extends Construct { + + private icav2_jwt_ssm_parameter_path: string; + private icav2_jwt_secret_arn_value: string; + public readonly icav2_copy_batch_state_machine_arn: string; + + constructor(scope: Construct, id: string, props: ICAv2CopyBatchUtilityConstructProps) { + super(scope, id); + + // Import external ssm parameters + this.set_jwt_secret_arn_object(props.icav2_jwt_ssm_parameter_path); + + // Generate lambda layer + const lambda_layer = new LambdaLayerConstruct( + this, 'lambda_layer', { + layer_directory: props.lambdas_layer_path, + layer_name: 'icav2_copy_batch_utility_tools' + }); + + // Manifest inverter lambda + const manifest_inverter_lambda = new PythonFunction(this, 'manifest_inverter_lambda', { + entry: props.manifest_handler_lambda_path, + runtime: lambda.Runtime.PYTHON_3_11, + index: 'handler.py', + handler: 'handler', + memorySize: 1024, + // @ts-ignore + layers: [lambda_layer.lambda_layer_version_obj] + }); + + // Copy batch data handler lambda + const copy_batch_data_lambda = new PythonFunction(this, 'copy_batch_data_lambda_python_function', { + entry: props.copy_batch_data_lambda_path, + runtime: lambda.Runtime.PYTHON_3_11, + index: 'handler.py', + handler: 'handler', + memorySize: 1024, + // @ts-ignore + layers: [lambda_layer.lambda_layer_version_obj], + // @ts-ignore + timeout: Duration.seconds(300), + }); + + // Job Status Handler + const job_status_handler_lambda = new PythonFunction(this, 'job_status_handler_lambda', { + entry: props.job_status_handler_lambda_path, + runtime: lambda.Runtime.PYTHON_3_11, + index: 'handler.py', + handler: 'handler', + memorySize: 1024, + // @ts-ignore + layers: [lambda_layer.lambda_layer_version_obj], + // @ts-ignore // We go through at least 10 jobs now to check if they're completed + timeout: Duration.seconds(300), + }); + + // Specify the statemachine and replace the arn placeholders with the lambda arns defined above + const stateMachine = new sfn.StateMachine(this, 'copy_batch_state_machine', { + // Definition Template + definitionBody: DefinitionBody.fromFile(props.definition_body_path), + // Definition Substitutions + definitionSubstitutions: { + '__manifest_inverter_lambda_arn__': manifest_inverter_lambda.functionArn, + '__copy_batch_data_lambda_arn__': copy_batch_data_lambda.functionArn, + '__job_status_handler_lambda_arn__': job_status_handler_lambda.functionArn, + }, + }); + + // Add execution permissions to stateMachine role + stateMachine.addToRolePolicy( + new iam.PolicyStatement( + { + resources: [ + manifest_inverter_lambda.functionArn, + copy_batch_data_lambda.functionArn, + job_status_handler_lambda.functionArn, + ], + actions: [ + 'lambda:InvokeFunction', + ], + }, + ), + ); + + // Update lambda policies + [ + copy_batch_data_lambda, + job_status_handler_lambda + ].forEach( + lambda_function => { + this.add_icav2_secrets_permissions_to_lambda( + lambda_function, + ); + } + ); + + // Set outputs + this.icav2_copy_batch_state_machine_arn = stateMachine.stateMachineArn; + } + + private set_jwt_secret_arn_object(icav2_jwt_ssm_parameter_path: string) { + const icav2_jwt_ssm_parameter = ssm.StringParameter.fromStringParameterName( + this, + 'get_jwt_secret_arn_value', + icav2_jwt_ssm_parameter_path, + ); + + this.icav2_jwt_ssm_parameter_path = icav2_jwt_ssm_parameter.parameterArn; + this.icav2_jwt_secret_arn_value = icav2_jwt_ssm_parameter.stringValue; + + } + + private add_icav2_secrets_permissions_to_lambda( + lambda_function: lambda.Function | PythonFunction, + ) { + /* + Add the statement that allows + */ + lambda_function.addToRolePolicy( + // @ts-ignore + new iam.PolicyStatement( + { + resources: [ + this.icav2_jwt_secret_arn_value, + this.icav2_jwt_ssm_parameter_path, + ], + actions: [ + 'secretsmanager:GetSecretValue', + 'ssm:GetParameter', + ], + }, + ), + ); + } + +} + diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/lambda_layer.ts b/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/lambda_layer.ts new file mode 100644 index 000000000..3f9305bd2 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/constructs/lambda_layer.ts @@ -0,0 +1,45 @@ + +import { Construct } from 'constructs' +import { PythonLayerVersion } from '@aws-cdk/aws-lambda-python-alpha'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +export interface LambdaLayerConstructProps { + layer_directory: string + layer_name: string +} + +export class LambdaLayerConstruct extends Construct { + + public readonly lambda_layer_arn: string; + public readonly lambda_layer_version_obj: PythonLayerVersion; + + constructor(scope: Construct, id: string, props: LambdaLayerConstructProps) { + super(scope, id); + + this.lambda_layer_version_obj = new PythonLayerVersion( + this, + `tool_layer_${props.layer_name}`, + { + entry: props.layer_directory, + compatibleRuntimes: [lambda.Runtime.PYTHON_3_11], + compatibleArchitectures: [lambda.Architecture.X86_64], + license: 'GPL3', + description: `A layer to enable the lambda runtime ${props.layer_name}`, + bundling: { + commandHooks: { + beforeBundling(inputDir: string, outputDir: string): string[] { + return []; + }, + afterBundling(inputDir: string, outputDir: string): string[] { + return [ + `python -m pip install ${inputDir} -t ${outputDir}`, + ]; + }, + }, + }, + }); + + // Set outputs + this.lambda_layer_arn = this.lambda_layer_version_obj.layerVersionArn; + } +} \ No newline at end of file diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/stacks/icav2_copy_batch_utility_stack.ts b/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/stacks/icav2_copy_batch_utility_stack.ts new file mode 100644 index 000000000..16aa1b376 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/lib/stacks/icav2_copy_batch_utility_stack.ts @@ -0,0 +1,61 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { ICAv2CopyBatchUtilityConstruct } from '../constructs/icav2_copy_batch_utility'; + +// import * as sqs from 'aws-cdk-lib/aws-sqs'; +interface ICAv2CopyBatchUtilityStackStackProps extends cdk.StackProps { + icav2_jwt_ssm_parameter_path: string; // "/icav2/umccr-prod/service-production-jwt-token-secret-arn" + icav2_copy_batch_state_machine_ssm_parameter_path: string; +} + +export class ICAv2CopyBatchUtilityStack extends cdk.Stack { + + public readonly icav2_copy_batch_state_machine_arn: string + public readonly icav2_copy_batch_state_machine_ssm_parameter_path: string + + constructor(scope: Construct, id: string, props: ICAv2CopyBatchUtilityStackStackProps) { + super(scope, id, props); + + // Generate icav2 copy batch stack + const icav2_copy_batch_state_machine = new ICAv2CopyBatchUtilityConstruct( + this, + 'icav2_copy_batch_state_machine', + { + copy_batch_data_lambda_path: __dirname + '/../../../lambdas/copy_batch_data_handler', + definition_body_path: __dirname + '/../../../step_functions_templates/copy_batch_state_machine.json', + icav2_jwt_ssm_parameter_path: props.icav2_jwt_ssm_parameter_path, + job_status_handler_lambda_path: __dirname + '/../../../lambdas/job_status_handler', + lambdas_layer_path: __dirname + '/../../../layers', + manifest_handler_lambda_path: __dirname + '/../../../lambdas/manifest_handler' + } + ) + + // Set Attributes + this.icav2_copy_batch_state_machine_arn = icav2_copy_batch_state_machine.icav2_copy_batch_state_machine_arn + this.icav2_copy_batch_state_machine_ssm_parameter_path = props.icav2_copy_batch_state_machine_ssm_parameter_path + + // Generate ssm parameter + this.set_ssm_parameter_obj_for_state_machine( + icav2_copy_batch_state_machine.icav2_copy_batch_state_machine_arn + ) + + } + + private set_ssm_parameter_obj_for_state_machine( + state_machine_arn: string, + ): ssm.StringParameter { + /* + Generate the ssm parameter for the state machine arn + */ + return new ssm.StringParameter( + this, + 'state_machine_arn_ssm_parameter', + { + parameterName: this.icav2_copy_batch_state_machine_ssm_parameter_path, + stringValue: state_machine_arn, + }, + ); + } + +} diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/package.json b/lib/workload/stateless/icav2_copy_batch_utility/deploy/package.json new file mode 100644 index 000000000..539e65439 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/package.json @@ -0,0 +1,27 @@ +{ + "name": "icav2_copy_batch_utility", + "version": "0.1.0", + "bin": { + "icav2_copy_batch_utility": "bin/icav2_copy_batch_utility.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "20.11.16", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "aws-cdk": "2.128.0", + "ts-node": "^10.9.2", + "typescript": "~5.3.3" + }, + "dependencies": { + "aws-cdk-lib": "2.128.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} \ No newline at end of file diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/test/icav2_copy_batch_utility.test.ts b/lib/workload/stateless/icav2_copy_batch_utility/deploy/test/icav2_copy_batch_utility.test.ts new file mode 100644 index 000000000..111425cbe --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/test/icav2_copy_batch_utility.test.ts @@ -0,0 +1,17 @@ +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as Icav2CopyBatchUtility from '../lib/icav2_copy_batch_utility-stack'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/icav2_copy_batch_utility_stack.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// // WHEN +// const stack = new Icav2CopyBatchUtility.Icav2CopyBatchUtilityStack(app, 'MyTestStack'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/lib/workload/stateless/icav2_copy_batch_utility/deploy/tsconfig.json b/lib/workload/stateless/icav2_copy_batch_utility/deploy/tsconfig.json new file mode 100644 index 000000000..aaa7dc510 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/deploy/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/lib/workload/stateless/icav2_copy_batch_utility/images/step_functions_example.png b/lib/workload/stateless/icav2_copy_batch_utility/images/step_functions_example.png new file mode 100755 index 0000000000000000000000000000000000000000..11f7c7899aedafc4c8ebae8dbc34f070a38b03c3 GIT binary patch literal 67595 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

worAky( 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 YJXYAGGg(TY)D`fNmsOU5Nt=H7A6QcA6951J literal 0 HcmV?d00001 diff --git a/lib/workload/stateless/icav2_copy_batch_utility/lambdas/copy_batch_data_handler/handler.py b/lib/workload/stateless/icav2_copy_batch_utility/lambdas/copy_batch_data_handler/handler.py new file mode 100644 index 000000000..92e9a2b93 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/lambdas/copy_batch_data_handler/handler.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +""" +Given a dest URI and list of source data ids, copy each of the data ids from the source URI to the destination URIs +Returns a list of job IDs + +{ + "dest_uri": "icav2://src_project_id/path/to/dest/", + "source_uris": [ + "icav2://project_id/path/to/src/file1", + "icav2://project_id/path/to/src/file2", + ] +} +""" + + +# Imports +from icav2_copy_batch_utility_tools.utils.aws_ssm_helpers import set_icav2_env_vars +from icav2_copy_batch_utility_tools.utils.job_helpers import submit_copy_job + + +def handler(event, context): + """ + Read in the event and collect the workflow session details + """ + # Set ICAv2 configuration from secrets + set_icav2_env_vars() + + return { + "dest_uri": event.get("dest_uri"), + "source_uris": event.get("source_uris"), + "job_attempt_counter": 1, + "job_id": submit_copy_job( + event.get("dest_uri"), + event.get("source_uris") + ), + "failed_jobs_list": [] + } + + +# if __name__ == "__main__": +# import json +# print( +# json.dumps( +# handler( +# event={ +# "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307125959/Reports/", +# "source_uris": [ +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Adapter_Metrics.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Demultiplex_Stats.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Quality_Metrics.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Index_Hopping_Counts.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Adapter_Cycle_Metrics.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/SampleSheet.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/RunInfo.xml", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/fastq_list.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/IndexMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Top_Unknown_Barcodes.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Demultiplex_Tile_Stats.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/report.html", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Quality_Tile_Metrics.csv" +# ] +# }, +# context=None +# ) +# ) +# ) \ No newline at end of file diff --git a/lib/workload/stateless/icav2_copy_batch_utility/lambdas/job_status_handler/handler.py b/lib/workload/stateless/icav2_copy_batch_utility/lambdas/job_status_handler/handler.py new file mode 100644 index 000000000..38d88374c --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/lambdas/job_status_handler/handler.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 + +""" +Lambda to determine if a given ICAv2 Copy Job has finished. +Returns the status of the job which is one of the following +* INITIALIZED +* WAITING_FOR_RESOURCES +* RUNNING +* STOPPED +* SUCCEEDED +* PARTIALLY_SUCCEEDED +* FAILED + +The event input is +{ + "job_id": "12345" # FIXME +} + +""" +from pathlib import Path +from typing import List + +# Wrapica imports +from wrapica.job import get_job + +# Local imports +from icav2_copy_batch_utility_tools.utils.job_helpers import submit_copy_job +from icav2_copy_batch_utility_tools.utils.aws_ssm_helpers import set_icav2_env_vars + +# Globals +SUCCESS_STATES = [ + "SUCCEEDED" +] +TERMINAL_STATES = [ + "STOPPED", + "FAILED", + "PARTIALLY_SUCCEEDED" +] +RUNNING_STATES = [ + "INITIALIZED", + "WAITING_FOR_RESOURCES", + "RUNNING" +] + +MAX_JOB_ATTEMPT_COUNTER = 5 +MAX_JOBS_HANDLED = 10 + + +def handler(event, context): + """ + + :param event: + :param context: + :return: + """ + set_icav2_env_vars() + + # Get events + job_list: List = event.get("job_list") + job_list_index: int = event.get("job_list_index") + + # Initialise the number of jobs handled + num_jobs_handled = 0 + job_iterables_to_bump = [] # List of job iterables we can move to the end of the list + wait = True # Whether or not to go to wait step or call this function again + + # Loop through the job list starting from the job list index + for job_iter in range(job_list_index, len(job_list)): + # Get job object + job = get_job(job_list[job_iter].get("job_id")) + + # Return status + if job.status in SUCCESS_STATES: + job_status = True + elif job.status in TERMINAL_STATES: + job_status = False + elif job.status in RUNNING_STATES: + job_status = None + else: + raise Exception("Unknown job status: {}".format(job.status)) + + # Check iterator + if job_status is False: + if job_list[job_iter].get("job_attempt_counter", 0) > MAX_JOB_ATTEMPT_COUNTER: + job_list[job_iter]["job_status"] = False + # Update the failed jobs list + job_list[job_iter]["failed_jobs_list"].append(job_list[job_iter].get("job_id")) + + # Get inputs to this job + dest_uri = job_list[job_iter].get("dest_uri") + source_uris = job_list[job_iter].get("source_uris") + job_list[job_iter]["job_id"] = submit_copy_job( + dest_uri=dest_uri, + source_uris=source_uris, + ) + + # Set job status to running state + job_list[job_iter]["job_status"] = None + + # Increment the job attempt counter + job_list[job_iter]["job_attempt_counter"] += 1 + + job_iterables_to_bump.append(job_iter) + + else: + # Update the job status for the job + job_list[job_iter]["job_status"] = job_status + + # Increment the number of jobs handled + num_jobs_handled += 1 + + # If the number of jobs handled is greater than the maximum number of jobs handled + if num_jobs_handled > MAX_JOBS_HANDLED: + wait = False + break + + # Move jobs in the job_iterables_to_bump to the end of the list + # These have just been submitted (again) and we dont want to look at them at the next iteration + job_list_clean = [job_list[i] for i in range(len(job_list)) if i not in job_iterables_to_bump] + job_list = job_list_clean + [job_list[i] for i in job_iterables_to_bump] + del job_list_clean + + # Move all passed and failed copy jobs to the front + passed_jobs = list( + filter( + lambda job_iter_obj: job_iter_obj.get("job_status") is True, + job_list + ) + ) + + failed_jobs = list( + filter( + lambda job_iter_obj: job_iter_obj.get("job_status") is False, + job_list + ) + ) + + running_jobs = list( + filter( + lambda job_iter_obj: job_iter_obj.get("job_status") is None, + job_list + ) + ) + + # We set the index of the list to the length of the passed jobs + job_list_index = len(passed_jobs) + len(failed_jobs) + + # Push all running jobs to the back of the list + job_list = passed_jobs + failed_jobs + running_jobs + + # Return the job list + return { + "job_list": job_list, + "job_list_index": job_list_index, + "counters": { + "jobs_failed": len(failed_jobs), + "jobs_running": len(running_jobs), + "jobs_passed": len(passed_jobs) + }, + "wait": wait + } + + +# if __name__ == "__main__": +# import json +# +# print( +# json.dumps( +# handler( +# event={ +# "job_list": [ +# { +# "job_attempt_counter": 1, +# "job_id": "d80ea8f4-b2a4-4b5f-840f-2426584d0495", +# "failed_jobs_list": [], +# "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/InterOp/", +# "source_uris": [ +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/AlignmentMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/EmpiricalPhasingMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/CorrectedIntMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/BasecallingMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ErrorMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ExtendedTileMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/OpticalModelMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/QMetricsByLaneOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ExtractionMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/PFGridMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ImageMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/TileMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/QMetrics2030Out.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/FWHMGridMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/QMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/EventMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/RegistrationMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/IndexMetricsOut.bin" +# ], +# "job_status": True +# }, +# { +# "job_attempt_counter": 1, +# "job_id": "7c240086-6c62-4328-96f1-66b7c6430fc4", +# "failed_jobs_list": [], +# "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/Reports/", +# "source_uris": [ +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Adapter_Metrics.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Demultiplex_Stats.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Quality_Metrics.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Index_Hopping_Counts.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Adapter_Cycle_Metrics.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/SampleSheet.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/RunInfo.xml", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/fastq_list.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/IndexMetricsOut.bin", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Top_Unknown_Barcodes.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Demultiplex_Tile_Stats.csv", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/report.html", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Quality_Tile_Metrics.csv" +# ], +# "job_status": True +# }, +# { +# "job_attempt_counter": 1, +# "job_id": "f768bacc-3639-4417-a55a-937ef64c02e7", +# "failed_jobs_list": [], +# "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/Samples/Lane_1/L2301368/", +# "source_uris": [ +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R1_001.fastq.gz", +# "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R2_001.fastq.gz" +# ], +# "job_status": True +# }, +# ... +# ], +# "job_list_index": 8, +# "counters": { +# "jobs_failed": 0, +# "jobs_running": 42, +# "jobs_passed": 8 +# }, +# "wait": False +# } +# , +# context=None +# ) +# ) +# ) +# +# # { +# # "job_list": [ +# # { +# # "job_attempt_counter": 1, +# # "job_id": "d80ea8f4-b2a4-4b5f-840f-2426584d0495", +# # "failed_jobs_list": [], +# # "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/InterOp/", +# # "source_uris": [ +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/AlignmentMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/EmpiricalPhasingMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/CorrectedIntMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/BasecallingMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ErrorMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ExtendedTileMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/OpticalModelMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/QMetricsByLaneOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ExtractionMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/PFGridMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/ImageMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/TileMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/QMetrics2030Out.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/FWHMGridMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/QMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/EventMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/RegistrationMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-runs/bssh_aps2-sh-prod_3661659/InterOp/IndexMetricsOut.bin" +# # ], +# # "job_status": true +# # }, +# # { +# # "job_attempt_counter": 1, +# # "job_id": "7c240086-6c62-4328-96f1-66b7c6430fc4", +# # "failed_jobs_list": [], +# # "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/Reports/", +# # "source_uris": [ +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Adapter_Metrics.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Demultiplex_Stats.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Quality_Metrics.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Index_Hopping_Counts.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Adapter_Cycle_Metrics.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/SampleSheet.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/RunInfo.xml", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/fastq_list.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/IndexMetricsOut.bin", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Top_Unknown_Barcodes.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Demultiplex_Tile_Stats.csv", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/report.html", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Reports/Quality_Tile_Metrics.csv" +# # ], +# # "job_status": true +# # }, +# # { +# # "job_attempt_counter": 1, +# # "job_id": "f768bacc-3639-4417-a55a-937ef64c02e7", +# # "failed_jobs_list": [], +# # "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/Samples/Lane_1/L2301368/", +# # "source_uris": [ +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R1_001.fastq.gz", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301368/L2301368_S1_L001_R2_001.fastq.gz" +# # ], +# # "job_status": true +# # }, +# # { +# # "job_attempt_counter": 1, +# # "job_id": "b99b7780-718a-4f30-9441-57244d5f228f", +# # "failed_jobs_list": [], +# # "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3661659/20240307135959/Samples/Lane_1/L2301369/", +# # "source_uris": [ +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301369/L2301369_S2_L001_R1_001.fastq.gz", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_d24651_4c90dc-BclConvert v4_2_7-b719c8d9-5e6d-49e6-a8be-ca17b5e9d40b/output/Samples/Lane_1/L2301369/L2301369_S2_L001_R2_001.fastq.gz" +# # ], +# # "job_status": true +# # }, +# # ... +# # ], +# # "job_list_index": 18, +# # "counters": { +# # "jobs_failed": 0, +# # "jobs_running": 32, +# # "jobs_passed": 18 +# # }, +# # "wait": false +# # } diff --git a/lib/workload/stateless/icav2_copy_batch_utility/lambdas/manifest_handler/handler.py b/lib/workload/stateless/icav2_copy_batch_utility/lambdas/manifest_handler/handler.py new file mode 100644 index 000000000..9c7e119ac --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/lambdas/manifest_handler/handler.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +""" +Take a dictionary of source uris where each value is a list of destination uris. + +Flip the manifest so that instead the destination uri is the key and the source uris are the value. + +In this case, the source uris should be files that reside directory underneath the destination uri. + +This allows the copy batch data handler to then easily process each key as a job, since we can have multiple data ids +be copied to the one folder: + +Event will look something like either + + + +{ + "manifest": { + "icav2://project_id/path/to/src/file1": [ + "icav2://project_id/path/to/dest/folder1/", + "icav2://project_id/path/to/dest/folder2/", + ], + "icav2://project_id/path/to/src/file2": [ + "icav2://project_id/path/to/dest/folder2/", + "icav2://project_id/path/to/dest/folder3/", + ] + }, + "manifest_b64gz": "H4sIAAAAAAAAA9XQsU7DMBSF4T1PEXXmYvvaN7a7URhAKhMSICFkXdtJFSlNqqSJVCHenQILEzPs +Z/jO/1aU5apNvOBaiIi6iaQqyI4QjHcEnFMCp1VSWLuUEUXb7XvgnrvTVE8CtVKqCldSScIglcWw +uX3c3tPNw7MNjVJsfNDKN9bAJnXXQ7/U47FcTMBgwaJmYyIBRqzBSGfBRSTwmDJrVmSyF8N8PMxH +sR12k3jisW/73XTZDbvVunw563/4LXmqXYOgMWswyVbAGg2kih1lrp2N9OUPh7Hd83gSKFH//kHo +inxF7nNqJErLMeV5bvM3aHUWvF78r4h3fTP8rYDFe/EBi+cuYYkCAAA= +" +} + +So we flip this to be + +{ + "icav2://project_id/path/to/dest/folder1/": [ + "icav2://project_id/path/to/src/file1", + ], + "icav2://project_id/path/to/dest/folder2/": [ + "icav2://project_id/path/to/src/file1", + "icav2://project_id/path/to/src/file2", + ], + "icav2://project_id/path/to/dest/folder3/": [ + "icav2://project_id/path/to/src/file2", + ] +} + +Note the output must be decompressed + +Convert to an array as this will help AWS Step Functions deploy the manifest for the copy batch data handler. + +We don't convert the uris to data ids incase there are too many, and we worry about lambda time outs, +that can be done by the lambda that generates the copy job + +[ + { + "dest_uri": "icav2://project_id/path/to/dest/folder1/", + "source_uris": [ + "icav2://project_id/path/to/src/file1", + ], + }, + { + "dest_uri": "icav2://project_id/path/to/dest/folder2/", + "source_uris": [ + "icav2://project_id/path/to/src/file1", + "icav2://project_id/path/to/src/file2", + ], + }, + { + "dest_uri": "icav2://project_id/path/to/dest/folder3/", + "source_uris": [ + "icav2://project_id/path/to/src/file2", + ] + } +] + +""" +from functools import reduce +from typing import Dict, List +from icav2_copy_batch_utility_tools.utils.compression_helpers import decompress_dict + + +def handler(event: Dict, context) -> List[Dict]: + """ + Flip the manifest and return as a list + :param event: + :param context: + :return: + """ + + # Check if we have a manifest + if event.get("manifest", None) and isinstance(event.get("manifest", None), dict): + manifest = event.get("manifest") + elif event.get("manifest_b64gz", None) and isinstance(event.get("manifest_b64gz", None), str): + manifest = decompress_dict(event.get("manifest_b64gz")) + else: + raise ValueError("No manifest found in event") + + # Collect all values from all keys + all_destination_uris = list( + set( + reduce( + lambda list_1, list_2: list_1 + list_2, + map( + lambda dest_uri_iter: dest_uri_iter, + manifest.values() + ), + [] + ) + ) + ) + + # Now invert our manifest by generating a per-destination uri list of source uris + source_uris_by_dest = {} + for dest_uri in all_destination_uris: + source_uris_by_dest[dest_uri] = list( + filter( + lambda source_uri_iter_filter: dest_uri in manifest[source_uri_iter_filter], + manifest.keys() + ) + ) + + # Convert the dict to a list where the key is "dest_uri" and "source_uris" represents the value + return ( + sorted( + list( + map( + lambda dest_uri_iter_kv: { + "dest_uri": dest_uri_iter_kv[0], + "source_uris": dest_uri_iter_kv[1] + }, + source_uris_by_dest.items() + ) + ), + key=lambda x: x.get("dest_uri") + ) + ) + + +# if __name__ == "__main__": +# import json +# +# print( +# json.dumps( +# handler( +# event={ +# "manifest": { +# "icav2://project_id/path/to/src/file1": [ +# "icav2://project_id/path/to/dest/folder1/", +# "icav2://project_id/path/to/dest/folder2/", +# ], +# "icav2://project_id/path/to/src/file2": [ +# "icav2://project_id/path/to/dest/folder2/", +# "icav2://project_id/path/to/dest/folder3/", +# ] +# } +# }, +# context=None +# ), +# indent=4 +# ) +# ) +# +# manifest_b64gz = """ +# H4sIAAAAAAAAA9XQsU7DMBSF4T1PEXXmYvvaN7a7URhAKhMSICFkXdtJFSlNqqSJVCHenQILEzPs +# Z/jO/1aU5apNvOBaiIi6iaQqyI4QjHcEnFMCp1VSWLuUEUXb7XvgnrvTVE8CtVKqCldSScIglcWw +# uX3c3tPNw7MNjVJsfNDKN9bAJnXXQ7/U47FcTMBgwaJmYyIBRqzBSGfBRSTwmDJrVmSyF8N8PMxH +# sR12k3jisW/73XTZDbvVunw563/4LXmqXYOgMWswyVbAGg2kih1lrp2N9OUPh7Hd83gSKFH//kHo +# inxF7nNqJErLMeV5bvM3aHUWvF78r4h3fTP8rYDFe/EBi+cuYYkCAAA= +# """.replace("\n", "") +# +# print( +# json.dumps( +# handler( +# event={ +# "manifest_b64gz": manifest_b64gz +# }, +# context=None +# ), +# indent=4 +# ) +# ) +# +# # [ +# # { +# # "dest_uri": "icav2://project_id/path/to/dest/folder1/", +# # "source_uris": [ +# # "icav2://project_id/path/to/src/file1" +# # ] +# # }, +# # { +# # "dest_uri": "icav2://project_id/path/to/dest/folder2/", +# # "source_uris": [ +# # "icav2://project_id/path/to/src/file1", +# # "icav2://project_id/path/to/src/file2" +# # ] +# # }, +# # { +# # "dest_uri": "icav2://project_id/path/to/dest/folder3/", +# # "source_uris": [ +# # "icav2://project_id/path/to/src/file2" +# # ] +# # } +# # ] +# +# # [ +# # { +# # "dest_uri": "icav2://7595e8f2-32d3-4c76-a324-c6a85dae87b5/ilmn_primary/2023/231116_A01052_0172_BHVLM5DSX7/3659658/20240207abcduuid/Logs/", +# # "source_uris": [ +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_f11a49_319f74-BclConvert v4_2_7-723a44b5-2b2e-4087-8b25-92cda3a154d9/output/Logs/Warnings.log", +# # "icav2://b23fb516-d852-4985-adcc-831c12e8cd22/ilmn-analyses/231116_A01052_0172_BHVLM5DSX7_f11a49_319f74-BclConvert v4_2_7-723a44b5-2b2e-4087-8b25-92cda3a154d9/output/Logs/Info.log" +# # ] +# # } +# # ] + diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/poetry.lock b/lib/workload/stateless/icav2_copy_batch_utility/layers/poetry.lock new file mode 100644 index 000000000..816b53b06 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/layers/poetry.lock @@ -0,0 +1,760 @@ +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. + +[[package]] +name = "aws-requests-auth" +version = "0.4.3" +description = "AWS signature version 4 signing process for the python requests module" +optional = false +python-versions = "*" +files = [ + {file = "aws-requests-auth-0.4.3.tar.gz", hash = "sha256:33593372018b960a31dbbe236f89421678b885c35f0b6a7abfae35bb77e069b2"}, + {file = "aws_requests_auth-0.4.3-py2.py3-none-any.whl", hash = "sha256:646bc37d62140ea1c709d20148f5d43197e6bd2d63909eb36fa4bb2345759977"}, +] + +[package.dependencies] +requests = ">=0.14.0" + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "boto3" +version = "1.34.56" +description = "The AWS SDK for Python" +optional = false +python-versions = ">= 3.8" +files = [ + {file = "boto3-1.34.56-py3-none-any.whl", hash = "sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714"}, + {file = "boto3-1.34.56.tar.gz", hash = "sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"}, +] + +[package.dependencies] +botocore = ">=1.34.56,<1.35.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.10.0,<0.11.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.34.56" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "botocore-1.34.56-py3-none-any.whl", hash = "sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"}, + {file = "botocore-1.34.56.tar.gz", hash = "sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.19.19)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "libica" +version = "2.4.0" +description = "Python SDK for Illumina Connected Analytics" +optional = false +python-versions = ">=3.7" +files = [ + {file = "libica-2.4.0-py3-none-any.whl", hash = "sha256:c80927a1f4aa3f60b2c9faf6e8c22f72000bfdf096429d065ebabfce9928120a"}, + {file = "libica-2.4.0.tar.gz", hash = "sha256:436098a5d5c5e843428e9b1592db33bcd2dbc09b47ed83b9ca1d533f65803189"}, +] + +[package.dependencies] +boto3 = "*" +botocore = "*" +certifi = "*" +python-dateutil = "*" +PyYAML = "*" +requests = "*" +six = "*" +urllib3 = "*" + +[package.extras] +dev = ["black", "build", "detect-secrets", "ggshield", "mkdocs", "mkdocs-material", "openapi-spec-validator", "pdoc3", "pipdeptree", "pre-commit", "setuptools", "twine", "wheel"] +test = ["flake8", "mockito", "nose2", "pytest", "pytest-cov", "tox"] + +[[package]] +name = "mypy-boto3-secretsmanager" +version = "1.34.43" +description = "Type annotations for boto3.SecretsManager 1.34.43 service generated with mypy-boto3-builder 7.23.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-boto3-secretsmanager-1.34.43.tar.gz", hash = "sha256:abbf560775c2fe0dc383b7f70c16a1bf753d9b3ffc0caa5e35447e685783a68b"}, + {file = "mypy_boto3_secretsmanager-1.34.43-py3-none-any.whl", hash = "sha256:64e9df58f71072f0a912ecaca626683f4536da078caa204ac07928c4b1481b8b"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-ssm" +version = "1.34.47" +description = "Type annotations for boto3.SSM 1.34.47 service generated with mypy-boto3-builder 7.23.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-boto3-ssm-1.34.47.tar.gz", hash = "sha256:be70cc32f9a07e6701746ebe65fba14d59c3f24a8511d275fd8322c9365f2270"}, + {file = "mypy_boto3_ssm-1.34.47-py3-none-any.whl", hash = "sha256:6517b1dc01e3ffe48a251c91e2a7fb6801223baf4a8cf1600411f9e132422297"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-stepfunctions" +version = "1.34.0" +description = "Type annotations for boto3.SFN 1.34.0 service generated with mypy-boto3-builder 7.21.0" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-boto3-stepfunctions-1.34.0.tar.gz", hash = "sha256:06d2296cee750d17cb62171420eea4614f20f29be45ee361854f8b599a6e8110"}, + {file = "mypy_boto3_stepfunctions-1.34.0-py3-none-any.whl", hash = "sha256:ecc1e674c1c89e0559e8dbf3fda81295642b13766db30d42968a986ae6a5e952"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyarrow" +version = "15.0.0" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyarrow-15.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0a524532fd6dd482edaa563b686d754c70417c2f72742a8c990b322d4c03a15d"}, + {file = "pyarrow-15.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60a6bdb314affa9c2e0d5dddf3d9cbb9ef4a8dddaa68669975287d47ece67642"}, + {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66958fd1771a4d4b754cd385835e66a3ef6b12611e001d4e5edfcef5f30391e2"}, + {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f500956a49aadd907eaa21d4fff75f73954605eaa41f61cb94fb008cf2e00c6"}, + {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6f87d9c4f09e049c2cade559643424da84c43a35068f2a1c4653dc5b1408a929"}, + {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85239b9f93278e130d86c0e6bb455dcb66fc3fd891398b9d45ace8799a871a1e"}, + {file = "pyarrow-15.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b8d43e31ca16aa6e12402fcb1e14352d0d809de70edd185c7650fe80e0769e3"}, + {file = "pyarrow-15.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:fa7cd198280dbd0c988df525e50e35b5d16873e2cdae2aaaa6363cdb64e3eec5"}, + {file = "pyarrow-15.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8780b1a29d3c8b21ba6b191305a2a607de2e30dab399776ff0aa09131e266340"}, + {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0ec198ccc680f6c92723fadcb97b74f07c45ff3fdec9dd765deb04955ccf19"}, + {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036a7209c235588c2f07477fe75c07e6caced9b7b61bb897c8d4e52c4b5f9555"}, + {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2bd8a0e5296797faf9a3294e9fa2dc67aa7f10ae2207920dbebb785c77e9dbe5"}, + {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e8ebed6053dbe76883a822d4e8da36860f479d55a762bd9e70d8494aed87113e"}, + {file = "pyarrow-15.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d53a9d1b2b5bd7d5e4cd84d018e2a45bc9baaa68f7e6e3ebed45649900ba99"}, + {file = "pyarrow-15.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9950a9c9df24090d3d558b43b97753b8f5867fb8e521f29876aa021c52fda351"}, + {file = "pyarrow-15.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:003d680b5e422d0204e7287bb3fa775b332b3fce2996aa69e9adea23f5c8f970"}, + {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f75fce89dad10c95f4bf590b765e3ae98bcc5ba9f6ce75adb828a334e26a3d40"}, + {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca9cb0039923bec49b4fe23803807e4ef39576a2bec59c32b11296464623dc2"}, + {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ed5a78ed29d171d0acc26a305a4b7f83c122d54ff5270810ac23c75813585e4"}, + {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6eda9e117f0402dfcd3cd6ec9bfee89ac5071c48fc83a84f3075b60efa96747f"}, + {file = "pyarrow-15.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a3a6180c0e8f2727e6f1b1c87c72d3254cac909e609f35f22532e4115461177"}, + {file = "pyarrow-15.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:19a8918045993349b207de72d4576af0191beef03ea655d8bdb13762f0cd6eac"}, + {file = "pyarrow-15.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0ec076b32bacb6666e8813a22e6e5a7ef1314c8069d4ff345efa6246bc38593"}, + {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5db1769e5d0a77eb92344c7382d6543bea1164cca3704f84aa44e26c67e320fb"}, + {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2617e3bf9df2a00020dd1c1c6dce5cc343d979efe10bc401c0632b0eef6ef5b"}, + {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:d31c1d45060180131caf10f0f698e3a782db333a422038bf7fe01dace18b3a31"}, + {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:c8c287d1d479de8269398b34282e206844abb3208224dbdd7166d580804674b7"}, + {file = "pyarrow-15.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:07eb7f07dc9ecbb8dace0f58f009d3a29ee58682fcdc91337dfeb51ea618a75b"}, + {file = "pyarrow-15.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:47af7036f64fce990bb8a5948c04722e4e3ea3e13b1007ef52dfe0aa8f23cf7f"}, + {file = "pyarrow-15.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93768ccfff85cf044c418bfeeafce9a8bb0cee091bd8fd19011aff91e58de540"}, + {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ee87fd6892700960d90abb7b17a72a5abb3b64ee0fe8db6c782bcc2d0dc0b4"}, + {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001fca027738c5f6be0b7a3159cc7ba16a5c52486db18160909a0831b063c4e4"}, + {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:d1c48648f64aec09accf44140dccb92f4f94394b8d79976c426a5b79b11d4fa7"}, + {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:972a0141be402bb18e3201448c8ae62958c9c7923dfaa3b3d4530c835ac81aed"}, + {file = "pyarrow-15.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f01fc5cf49081426429127aa2d427d9d98e1cb94a32cb961d583a70b7c4504e6"}, + {file = "pyarrow-15.0.0.tar.gz", hash = "sha256:876858f549d540898f927eba4ef77cd549ad8d24baa3207cf1b72e5788b50e83"}, +] + +[package.dependencies] +numpy = ">=1.16.6,<2" + +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruamel-base" +version = "1.0.0" +description = "common routines for ruamel packages" +optional = false +python-versions = "*" +files = [ + {file = "ruamel.base-1.0.0-py2-none-any.whl", hash = "sha256:ff7fe471b3d715fe4f2c4663d9e6d82efbeb3cea8e72accb99fd4fa90df7e5f9"}, + {file = "ruamel.base-1.0.0-py3-none-any.whl", hash = "sha256:3613a90afcf0735540804af2a693f630a0bccebefec9b4023a39e88950bb294e"}, + {file = "ruamel.base-1.0.0.tar.gz", hash = "sha256:c041333a0f0f00cd6593eb36aa83abb1a9e7544e83ba7a42aa7ac7476cee5cf3"}, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.6" +files = [ + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +] + +[[package]] +name = "s3transfer" +version = "0.10.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.8" +files = [ + {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, + {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, +] + +[package.dependencies] +botocore = ">=1.33.2,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "verboselogs" +version = "1.7" +description = "Verbose logging level for Python's logging module" +optional = false +python-versions = "*" +files = [ + {file = "verboselogs-1.7-py2.py3-none-any.whl", hash = "sha256:d63f23bf568295b95d3530c6864a0b580cec70e7ff974177dead1e4ffbc6ff49"}, + {file = "verboselogs-1.7.tar.gz", hash = "sha256:e33ddedcdfdafcb3a174701150430b11b46ceb64c2a9a26198c76a156568e427"}, +] + +[[package]] +name = "websocket-client" +version = "1.7.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "wrapica" +version = "1.0.0" +description = "Secondary level functions for ICAv2 based off libica" +optional = false +python-versions = ">=3.11" +files = [ + {file = "wrapica-1.0.0-py3-none-any.whl", hash = "sha256:762a06699297634f21fa4a4fc0c20dc8fac4019c4f3e9435cb423091228a4c82"}, + {file = "wrapica-1.0.0.tar.gz", hash = "sha256:b7354bc55dfe735750a251a758b77b392fa26b9441235ae787b4f0d1710c85d0"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.10.0,<5" +libica = ">=2.3.0,<3" +PyJWT = ">=2.8.0,<3" +"ruamel.base" = ">=1.0.0,<2" +"ruamel.yaml" = ">=0.18.0,<0.19" +verboselogs = ">=1.7,<2" +websocket-client = ">=1.4.2,<2" + +[package.extras] +build = ["build"] +docs = ["sphinx (>=7.2.6,<8)", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=2.0.0,<3)", "toml-to-requirements"] +test = ["pytest", "pytest-mock"] +toml = ["tomli-w (>=1.0.0,<2)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "7ee4275c0a4ee116eabe5207eb419e0ef552c4a6f18f84a3e26671270f8d572f" diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/pyproject.toml b/lib/workload/stateless/icav2_copy_batch_utility/layers/pyproject.toml new file mode 100644 index 000000000..679c2de9d --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/layers/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "icav2_copy_batch_utility_tools" +version = "0.0.1" +description = "BSSH Manager Lambda Layers" +license = "GPL-3.0-or-later" +authors = [ + "Alexis Lucattini" +] +homepage = "https://github.com/umccr/orcabus" +repository = "https://github.com/umccr/orcabus" + +[tool.poetry.dependencies] +python = "^3.11" +boto3 = "^1.28" +botocore = "^1.31" +aws_requests_auth = "^0.4.3" +wrapica = ">=1.0.0" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +pyarrow = "^15.0.0" # Pandas throws a warning if this is not installed +pytest = "^7.0.0" # For testing only +# For typehinting only, not required at runtime +mypy-boto3-ssm = "^1.34" +mypy-boto3-secretsmanager = "^1.34" +mypy-boto3-stepfunctions = "^1.34" \ No newline at end of file diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/__init__.py b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/__init__.py b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/aws_ssm_helpers.py b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/aws_ssm_helpers.py new file mode 100644 index 000000000..b305509f5 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/aws_ssm_helpers.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" +Configuration handler + +Get ICAv2 configuration handler +""" + +# Standard imports +import typing +import boto3 +from os import environ + +# Local imports +from .globals import ICAV2_BASE_URL, ICAV2_ACCESS_TOKEN_URN_SSM_PATH + +# Type checking, only available as dev dependencies +if typing.TYPE_CHECKING: + from mypy_boto3_ssm import SSMClient + from mypy_boto3_secretsmanager import SecretsManagerClient + + +# AWS things +def get_ssm_client() -> 'SSMClient': + """ + Return SSM client + """ + return boto3.client("ssm") + + +def get_secrets_manager_client() -> 'SecretsManagerClient': + """ + Return Secrets Manager client + """ + return boto3.client("secretsmanager") + + +def get_ssm_parameter_value(parameter_path) -> str: + """ + Get the ssm parameter value from the parameter path + :param parameter_path: + :return: + """ + return get_ssm_client().get_parameter(Name=parameter_path)["Parameter"]["Value"] + + +def get_secret(secret_arn: str) -> str: + """ + Return secret value + """ + return get_secrets_manager_client().get_secret_value(SecretId=secret_arn)["SecretString"] + + +# Set the icav2 environment variables +def set_icav2_env_vars(): + """ + Set the icav2 environment variables + :return: + """ + environ["ICAV2_BASE_URL"] = ICAV2_BASE_URL + environ["ICAV2_ACCESS_TOKEN"] = get_secret( + get_ssm_parameter_value(ICAV2_ACCESS_TOKEN_URN_SSM_PATH) + ) diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/compression_helpers.py b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/compression_helpers.py new file mode 100644 index 000000000..bfa729f96 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/compression_helpers.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +""" +Miscellaneous utilities for parsing through compressed strings +""" + +import json +from base64 import b64encode, b64decode +import gzip +from typing import Dict, List, Union + + +def compress_dict(input_dict: Union[Dict, List]) -> str: + """ + Given a json input, compress to a base64 encoded string + + param: input_dict: input dictionary to compress + + Returns: gzipped compressed base64 encoded string + """ + + # Compress + return b64encode( + gzip.compress( + json.dumps(input_dict).encode('utf-8') + ) + ).decode("utf-8") + + +def decompress_dict(input_compressed_b64gz_str: str) -> Union[Dict, List]: + """ + Given a base64 encoded string, decompress and return the original dictionary + Args: + input_compressed_b64gz_str: + + Returns: decompressed dictionary or list + """ + + # Decompress + return json.loads( + gzip.decompress( + b64decode(input_compressed_b64gz_str.encode('utf-8')) + ) + ) diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/globals.py b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/globals.py new file mode 100644 index 000000000..136debdfb --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/globals.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +ICAV2_BASE_URL = "https://ica.illumina.com/ica/rest" + +# External Portal SSM Path +PORTAL_API_BASE_URL_SSM_PATH = "/data_portal/backend/api_domain_name" +PORTAL_METADATA_ENDPOINT = "https://{PORTAL_API_BASE_URL}/iam/metadata/" + +# SSM Paths (from this stack) +ICAV2_ACCESS_TOKEN_URN_SSM_PATH = "/icav2/umccr-prod/service-user-trial-jwt-token-secret-arn" +ICAV2_CACHE_PROJECT_ID_SSM_PATH = "/icav2/umccr-prod/cache_project_id" +ICAV2_CACHE_PROJECT_BCLCONVERT_OUTPUT_SSM_PATH = "/icav2/umccr-prod/cache_project_bclconvert_output_path" +ICAV2_CACHE_PROJECT_CTTSO_OUTPUT_SSM_PATH = "/icav2/umccr-prod/cache_project_cttso_fastq_path" diff --git a/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/job_helpers.py b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/job_helpers.py new file mode 100644 index 000000000..b75b03591 --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/layers/src/icav2_copy_batch_utility_tools/utils/job_helpers.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +""" +Submit a copy job +""" + +from pathlib import Path +from typing import List +from urllib.parse import urlparse + +# Wrapica imports +from wrapica.project_data import convert_icav2_uri_to_data_obj, project_data_copy_batch_handler + +# Local imports + + +def submit_copy_job(dest_uri: str, source_uris: List[str]) -> str: + # Rerun copy batch process + source_data_ids = list( + map( + lambda source_uri_iter: convert_icav2_uri_to_data_obj( + source_uri_iter + ).data.id, + source_uris + ) + ) + + dest_uri = urlparse(dest_uri) + + return project_data_copy_batch_handler( + source_data_ids=source_data_ids, + destination_project_id=dest_uri.netloc, + destination_folder_path=Path(dest_uri.path) + ).id diff --git a/lib/workload/stateless/icav2_copy_batch_utility/step_functions_templates/copy_batch_state_machine.json b/lib/workload/stateless/icav2_copy_batch_utility/step_functions_templates/copy_batch_state_machine.json new file mode 100644 index 000000000..3fdd819de --- /dev/null +++ b/lib/workload/stateless/icav2_copy_batch_utility/step_functions_templates/copy_batch_state_machine.json @@ -0,0 +1,188 @@ +{ + "Comment": "Deploy a set of ICAv2 copy batch jobs and wait until theyre finished", + "StartAt": "Move inputs", + "States": { + "Move inputs": { + "Type": "Pass", + "Parameters": { + "workflow_inputs.$": "$" + }, + "Next": "Flip Manifest" + }, + "Flip Manifest": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName": "${__manifest_inverter_lambda_arn__}", + "Payload.$": "$" + }, + "Retry": [ + { + "ErrorEquals": [ + "Lambda.ServiceException", + "Lambda.AWSLambdaException", + "Lambda.SdkClientException", + "Lambda.TooManyRequestsException" + ], + "IntervalSeconds": 1, + "MaxAttempts": 3, + "BackoffRate": 2 + } + ], + "ResultPath": "$.manifest_inverted_step", + "TimeoutSeconds": 20, + "InputPath": "$.workflow_inputs", + "ResultSelector": { + "manifest_inverted.$": "$.Payload" + }, + "Next": "Initialise Job Status Iterable Parameter" + }, + "Initialise Job Status Iterable Parameter": { + "Type": "Pass", + "ResultPath": "$.job_status_iterable_parameter", + "Result": { + "job_status_iterable": 0 + }, + "Next": "Initialise Wait Parameter" + }, + "Initialise Wait Parameter": { + "Type": "Pass", + "ResultPath": "$.wait_parameter", + "Result": { + "wait": false + }, + "Next": "Initialise counters" + }, + "Initialise counters": { + "Type": "Pass", + "Parameters": { + "jobs_passed": 0, + "jobs_failed": 0, + "jobs_running.$": "States.ArrayLength($.manifest_inverted_step.manifest_inverted)" + }, + "ResultPath": "$.counters", + "Next": "Process Manifest" + }, + "Process Manifest": { + "Type": "Map", + "Iterator": { + "StartAt": "Launch Copy Job Lambda", + "States": { + "Launch Copy Job Lambda": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "Payload.$": "$", + "FunctionName": "${__copy_batch_data_lambda_arn__}" + }, + "TimeoutSeconds": 300, + "Retry": [ + { + "ErrorEquals": [ + "Lambda.ServiceException", + "Lambda.AWSLambdaException", + "Lambda.SdkClientException", + "Lambda.TooManyRequestsException" + ], + "IntervalSeconds": 1, + "MaxAttempts": 3, + "BackoffRate": 2 + } + ], + "ResultSelector": { + "dest_uri.$": "$.Payload.dest_uri", + "source_uris.$": "$.Payload.source_uris", + "job_attempt_counter.$": "$.Payload.job_attempt_counter", + "job_id.$": "$.Payload.job_id", + "failed_jobs_list.$": "$.Payload.failed_jobs_list" + }, + "End": true + } + } + }, + "ItemsPath": "$.manifest_inverted_step.manifest_inverted", + "ResultPath": "$.job_list_with_attempt_counter", + "Next": "Wait 10 seconds before starting job loop" + }, + "Wait 10 seconds before starting job loop": { + "Type": "Wait", + "Seconds": 10, + "Next": "Update Job Statuses" + }, + "Update Job Statuses": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "Payload": { + "job_list.$": "$.job_list_with_attempt_counter", + "job_list_index.$": "$.job_status_iterable_parameter.job_status_iterable", + "counters.$": "$.counters" + }, + "FunctionName": "${__job_status_handler_lambda_arn__}" + }, + "TimeoutSeconds": 300, + "Retry": [ + { + "ErrorEquals": [ + "Lambda.ServiceException", + "Lambda.AWSLambdaException", + "Lambda.SdkClientException", + "Lambda.TooManyRequestsException" + ], + "IntervalSeconds": 1, + "MaxAttempts": 3, + "BackoffRate": 2 + } + ], + "ResultSelector": { + "job_list_with_attempt_counter.$": "$.Payload.job_list", + "counters.$": "$.Payload.counters", + "job_status_iterable_parameter": { + "job_status_iterable.$": "$.Payload.job_list_index" + }, + "wait_parameter": { + "wait.$": "$.Payload.wait" + } + }, + "Next": "Rerun update wait or finish" + }, + "Rerun update wait or finish": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.counters.jobs_running", + "NumericEquals": 0, + "Next": "Finish" + }, + { + "Variable": "$.wait_parameter.wait", + "BooleanEquals": false, + "Next": "Update Job Statuses" + } + ], + "Default": "Wait 60 seconds before retrying job status check" + }, + "Wait 60 seconds before retrying job status check": { + "Type": "Wait", + "Seconds": 60, + "Next": "Update Job Statuses" + }, + "Finish": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.counters.jobs_failed", + "NumericGreaterThan": 0, + "Next": "Fail" + } + ], + "Default": "Succeed" + }, + "Fail": { + "Type": "Fail" + }, + "Succeed": { + "Type": "Succeed" + } + } +} \ No newline at end of file