Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pythonfunction #11

Merged
merged 22 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ repos:
entry: bash -c "npm run eslint"
language: node
stages: [pre-push]
- id: python-lint
name: python-lint
entry: bash -c "cd src/packager && make lint"
language: system
stages: [pre-commit]
5 changes: 5 additions & 0 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 40 additions & 20 deletions .projenrc.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { awscdk } from 'projen';

const cdkVersion = '2.114.1';
const solutionName = 'omics-quilt-demo';
const project = new awscdk.AwsCdkTypeScriptApp({
cdkVersion: '2.114.1',
cdkVersion: cdkVersion,
majorVersion: 1,
defaultReleaseBranch: 'main',
description: 'Use CDK to create Quilt packages from AWS HealthOmics',
name: 'omics-quilt-demo',
name: solutionName,
projenrcTs: true,
deps: [
'aws-lambda',
`@aws-cdk/aws-lambda-python-alpha@^${cdkVersion}-alpha.0`,
'@aws-sdk/client-s3',
'@aws-sdk/client-sns',
'@aws-sdk/client-omics',
Expand All @@ -25,25 +29,41 @@ const project = new awscdk.AwsCdkTypeScriptApp({
'.env*',
'.DS_Store',
'test/__snapshots__/*',
'__pycache__', // Python
'*.pyc', // Python
],
});
project.tryFindObjectFile('.github/workflows/build.yml')!.addOverride('jobs.build.env', {
CI: 'true',
AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}',
AWS_SECRET_ACCESS_KEY: '${{ secrets.AWS_SECRET_ACCESS_KEY }}',
AWS_ACCOUNT_ID: '${{ secrets.AWS_ACCOUNT_ID }}',
AWS_DEFAULT_REGION: '${{ secrets.AWS_DEFAULT_REGION }}',
CDK_APP_NAME: '${{ secrets.CDK_APP_NAME }}',
CDK_DEFAULT_ACCOUNT: '${{ secrets.AWS_ACCOUNT_ID }}',
CDK_DEFAULT_REGION: '${{ secrets.AWS_DEFAULT_REGION }}',
CDK_DEFAULT_EMAIL: '${{ secrets.CDK_DEFAULT_EMAIL }}',
QUILT_CATALOG_DOMAIN: '${{ secrets.QUILT_CATALOG_DOMAIN }}',
override_file_key('.github/workflows/build.yml', 'jobs.build.env');
fix_deprecation_warning();
/*
const appTestTask = project.addTask('pytest', {
cwd: 'src/packager',
exec: 'make test',
});
// Fix Jest 29 warning about deprecated config in `globals`
project.jest!.config.transform ??= {};
project.jest!.config.transform['\\.ts$'] = [
'ts-jest',
project.jest?.config.globals['ts-jest'],
];
delete project.jest!.config.globals['ts-jest'];
const testTask = project.tasks.tryFind('test');
testTask?.spawn(appTestTask);
*/
project.synth();


function override_file_key(file: string, key: string) {
const KEYS = 'AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_ACCOUNT_ID AWS_DEFAULT_REGION CDK_APP_NAME CDK_DEFAULT_EMAIL QUILT_CATALOG_DOMAIN'.split(' ');
var opts: {[key: string]: string} = { CI: 'true' };
for (const k of KEYS) {
opts[k] = `\${{ secrets.${k} }}`;
}
opts.CDK_DEFAULT_ACCOUNT = opts.AWS_ACCOUNT_ID;
opts.CDK_DEFAULT_REGION = opts.AWS_DEFAULT_REGION;

project.tryFindObjectFile(file)?.addOverride(key, opts);
}

// Fix Jest 29 warning about deprecated config in `globals`
function fix_deprecation_warning() {
project.jest!.config.transform ??= {};
project.jest!.config.transform['\\.ts$'] = [
'ts-jest',
project.jest?.config.globals['ts-jest'],
];
delete project.jest!.config.globals['ts-jest'];
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}
1 change: 1 addition & 0 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class Constants {
READY2RUN_WORKFLOW_ID: '9500764',
MANIFEST_ROOT: 'fastq',
MANIFEST_SUFFIX: '.json',
QUILT_METADATA: 'quilt_metadata.json',
FASTQ_SENTINEL: 'out/bqsr_report/NA12878.hg38.recal_data.csv',
};

public static GET(key: string): any {
Expand Down
21 changes: 14 additions & 7 deletions src/omics-quilt.fastq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ export async function handler(event: any, context: any) {
return { message: 'Success' };
}

async function save_input(prefix: string, item: any, cc: Constants) {
const input_uri = cc.get('INPUT_S3_LOCATION') + '/' + prefix + '.json';
console.info(`Writing input to ${input_uri}`);
await Constants.SaveObjectURI(input_uri, item);
async function save_metadata(item: any, cc: Constants) {
const sentinel_file = cc.get('QUILT_METADATA)');
if (!sentinel_file) {
console.info('No QUILT_METADATA, skipping metadata save');
return;
}
console.info(`Writing input to ${sentinel_file}`);
await Constants.SaveObjectURI(sentinel_file, item);
}

async function run_workflow(
Expand All @@ -74,7 +78,6 @@ async function run_workflow(
cc: Constants,
) {
const _samplename = item.sample_name;
await save_input(_samplename, item, cc);
console.info(`Starting workflow for sample: ${_samplename}`);
const uuid = cc.get('TEST_UUID') || uuidv4();
const run_name = `${_samplename}.${uuid}.`;
Expand All @@ -95,7 +98,6 @@ async function run_workflow(
},
requestId: uuid,
};
await save_input(run_name + 'input', options, cc);
try {
console.debug(`Workflow options: ${JSON.stringify(options)}`);
if (cc.get('debug') === true) {
Expand All @@ -104,7 +106,12 @@ async function run_workflow(
const input: StartRunCommandInput = options;
const response = await start_omics_run(input);
console.info(`Workflow response: ${JSON.stringify(response)}`);
await save_input(run_name + 'output', response, cc);
const run_metadata = {
sample: item,
run: response,
workflow: options,
};
await save_metadata(run_metadata, cc);
}
} catch (e: any) {
console.error('Error : ' + e.toString());
Expand Down
71 changes: 57 additions & 14 deletions src/omics-quilt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as python from '@aws-cdk/aws-lambda-python-alpha';
import { Duration, RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib';
import { Rule } from 'aws-cdk-lib/aws-events';
import { SnsTopic } from 'aws-cdk-lib/aws-events-targets';
import {
AccountPrincipal,
ArnPrincipal,
ManagedPolicy,
PolicyStatement,
Role,
Expand All @@ -23,12 +25,16 @@ import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { type Construct } from 'constructs';
import { Constants } from './constants';

const PYTHON_FOLDER = `${__dirname}/packager`;
const PYTHON_INDEX = 'packager/index.py';

export class OmicsQuiltStack extends Stack {
public readonly inputBucket: Bucket;
public readonly outputBucket: Bucket;

public readonly manifest_prefix: string;
public readonly manifest_suffix: string;
public readonly packager_sentinel: string;

readonly cc: Constants;
readonly lambdaRole: Role;
Expand All @@ -42,10 +48,12 @@ export class OmicsQuiltStack extends Stack {
const manifest_root = this.cc.get('MANIFEST_ROOT');
this.manifest_prefix = `${manifest_root}/${this.cc.region}`;
this.manifest_suffix = this.cc.get('MANIFEST_SUFFIX');
this.packager_sentinel = this.cc.get('FASTQ_SENTINEL');

// Create Input/Output S3 buckets
this.inputBucket = this.makeBucket('input');
this.outputBucket = this.makeBucket('output');

this.makeParameter('INPUT_BUCKET_NAME', this.inputBucket.bucketName);
this.makeParameter('OUTPUT_BUCKET_NAME', this.outputBucket.bucketName);
// SNS Topic for Workflow notifications
Expand All @@ -68,6 +76,16 @@ export class OmicsQuiltStack extends Stack {
],
});
fastqLambda.addEventSource(fastqTrigger);

const packagerLambda = this.makePythonLambda('packager', {});
// TODO: trigger on Omics completion event, not report file
const packagerTrigger = new S3EventSource(this.outputBucket, {
events: [EventType.OBJECT_CREATED],
filters: [
{ suffix: this.packager_sentinel },
],
});
packagerLambda.addEventSource(packagerTrigger);
}

private makeParameter(name: string, value: any) {
Expand Down Expand Up @@ -118,7 +136,7 @@ export class OmicsQuiltStack extends Stack {
source: ['aws.omics'],
detailType: ['Run Status Change'],
detail: {
status: ['FAILED', 'COMPLETED', 'CREATED'],
status: ['*'],
},
},
},
Expand Down Expand Up @@ -156,31 +174,56 @@ export class OmicsQuiltStack extends Stack {
const bucket = new Bucket(this, name, bucketOptions);
bucket.grantDelete(this.principal);
bucket.grantReadWrite(this.principal);
const quilt_arn = this.cc.get('QUILT_ROLE_ARN');
if (quilt_arn) {
const quilt_principal = new ArnPrincipal(quilt_arn);
bucket.grantReadWrite(quilt_principal);
}
return bucket;
}

private makeLambda(name: string, env: object) {
const output = ['s3:/', this.outputBucket.bucketName, this.cc.app];
const input = ['s3:/', this.inputBucket.bucketName, this.manifest_prefix];
const default_env = {
OMICS_ROLE: this.omicsRole.roleArn,
OUTPUT_S3_LOCATION: output.join('/'),
INPUT_S3_LOCATION: input.join('/'),
WORKFLOW_ID: this.cc.get('READY2RUN_WORKFLOW_ID'),
ECR_REGISTRY: this.cc.getEcrRegistry(),
LOG_LEVEL: 'ALL',
};
// create merged env
const final_env = Object.assign(default_env, env);
return new NodejsFunction(this, name, {
runtime: Runtime.NODEJS_18_X,
role: this.lambdaRole,
timeout: Duration.seconds(60),
retryAttempts: 1,
environment: final_env,
environment: this.makeLambdaEnv(env),
});
}

private makePythonLambda(name: string, env: object) {
return new python.PythonFunction(this, name, {
entry: PYTHON_FOLDER,
index: PYTHON_INDEX,
runtime: Runtime.PYTHON_3_11,
role: this.lambdaRole,
timeout: Duration.seconds(60),
retryAttempts: 1,
environment: this.makeLambdaEnv(env),
bundling: {
assetExcludes: ['.mypy_cache', '.pytest_cache', '.tox', '__pycache__'],
},
});
}

private makeLambdaEnv(env: object) {
const output = ['s3:/', this.outputBucket.bucketName, this.cc.app];
const input = ['s3:/', this.inputBucket.bucketName, this.manifest_prefix];
const final_env = {
ECR_REGISTRY: this.cc.getEcrRegistry(),
INPUT_S3_LOCATION: input.join('/'),
LOG_LEVEL: 'ALL',
OMICS_ROLE: this.omicsRole.roleArn,
OUTPUT_S3_LOCATION: output.join('/'),
SENTINEL_FILE: this.packager_sentinel,
QUILT_METADATA: this.cc.get('QUILT_METADATA'),
WORKFLOW_ID: this.cc.get('READY2RUN_WORKFLOW_ID'),
...env,
};
return final_env;
}

private makeLambdaRole() {
const lambdaRole = new Role(this, `${this.cc.app}-lambda-role`, {
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
Expand Down
7 changes: 7 additions & 0 deletions src/packager/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[run]
branch = true
source = instance_scheduler

[report]
exclude_lines =
if TYPE_CHECKING:
3 changes: 3 additions & 0 deletions src/packager/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}
22 changes: 22 additions & 0 deletions src/packager/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
PROJECT=packager
sinclude ../Makefile.include

.PHONY: all clean lint install test watch

all: install test

clean:
rm -rf .pytest_cache

install:
poetry install

test: lint
poetry run pytest

watch:
poetry run ptw --now .

lint:
poetry run mypy packager
poetry run black .
3 changes: 3 additions & 0 deletions src/packager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Packager

Python Lambda to create a Quilt package from an S3 URI
Loading