diff --git a/apps/sandbox/lambda-nodejs/project.json b/apps/sandbox/lambda-nodejs/project.json index deb76abd7e..ffb505e0d1 100644 --- a/apps/sandbox/lambda-nodejs/project.json +++ b/apps/sandbox/lambda-nodejs/project.json @@ -1,7 +1,7 @@ { "name": "sandbox-lambda-nodejs", "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", + "projectType": "application", "targets": { "build": { "executor": "nx:run-commands", diff --git a/apps/sandbox/lambda-python/.aws-sam/build.toml b/apps/sandbox/lambda-python/.aws-sam/build.toml new file mode 100644 index 0000000000..b4461decb7 --- /dev/null +++ b/apps/sandbox/lambda-python/.aws-sam/build.toml @@ -0,0 +1,12 @@ +# This file is auto generated by SAM CLI build command + +[function_build_definitions.a4887bcd-4b8c-43ca-a4d1-9fe4d156fee4] +packagetype = "Image" +functions = ["HelloWorldFunction"] + +[function_build_definitions.a4887bcd-4b8c-43ca-a4d1-9fe4d156fee4.metadata] +Dockerfile = "Dockerfile" +DockerContext = "/workspaces/sage-monorepo/apps/sandbox/lambda-python" +DockerTag = "python3.13-v1" + +[layer_build_definitions] diff --git a/apps/sandbox/lambda-python/.gitignore b/apps/sandbox/lambda-python/.gitignore new file mode 100644 index 0000000000..4808264dbf --- /dev/null +++ b/apps/sandbox/lambda-python/.gitignore @@ -0,0 +1,244 @@ + +# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Build folder + +*/build/* + +# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode \ No newline at end of file diff --git a/apps/sandbox/lambda-python/Dockerfile b/apps/sandbox/lambda-python/Dockerfile new file mode 100644 index 0000000000..c34df21937 --- /dev/null +++ b/apps/sandbox/lambda-python/Dockerfile @@ -0,0 +1,8 @@ +FROM public.ecr.aws/lambda/python:3.13 + +COPY hello_world/app.py requirements.txt ${LAMBDA_TASK_ROOT}/ + +RUN python3.13 -m pip install --no-cache-dir -r requirements.txt -t . + +# Command can be overwritten by providing a different command in the template directly. +CMD ["app.lambda_handler"] diff --git a/apps/sandbox/lambda-python/README.md b/apps/sandbox/lambda-python/README.md new file mode 100644 index 0000000000..83209fad3f --- /dev/null +++ b/apps/sandbox/lambda-python/README.md @@ -0,0 +1,45 @@ +# Sandbox Lambda Python + +## Build the project + +```console +nx build sandbox-lambda-python +``` + +## Build the Docker image of the Lambda function + +```console +nx build-image sandbox-lambda-python +``` + +## Start the Lambda function locally with Docker Compose + +Starts the Lambda function in the foreground, allowing you to view logs and interact with it +directly. + +```console +nx serve sandbox-lambda-python +``` + +Starts the Lambda function in detached mode, running it in the background. This is useful if you +want to continue using the terminal for other tasks while the function runs. + +```console +nx serve-detach sandbox-lambda-python +``` + +## Invoke the Lambda function locally + +To invoke the Lambda function after starting it locally, use the following command: + +```console +nx run sandbox-lambda-python:invoke --event +``` + +Replace `` with the path to your JSON file containing the event payload relative +to the location of the project folder. For example, if your event payload is stored in a file +located at `events/event.json` relative to the project folder: + +```console +nx run sandbox-lambda-python:invoke --event events/event.json +``` diff --git a/apps/sandbox/lambda-python/__init__.py b/apps/sandbox/lambda-python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/sandbox/lambda-python/events/event.json b/apps/sandbox/lambda-python/events/event.json new file mode 100644 index 0000000000..a6197dea6c --- /dev/null +++ b/apps/sandbox/lambda-python/events/event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"hello world\"}", + "resource": "/hello", + "path": "/hello", + "httpMethod": "GET", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/hello", + "resourcePath": "/hello", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/apps/sandbox/lambda-python/hello_world/__init__.py b/apps/sandbox/lambda-python/hello_world/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/sandbox/lambda-python/hello_world/app.py b/apps/sandbox/lambda-python/hello_world/app.py new file mode 100644 index 0000000000..a04e2594c8 --- /dev/null +++ b/apps/sandbox/lambda-python/hello_world/app.py @@ -0,0 +1,33 @@ +import json + + +def lambda_handler(event, context): + """Sample pure Lambda function + + Parameters + ---------- + event: dict, required + API Gateway Lambda Proxy Input Format + + Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + + context: object, required + Lambda Context runtime methods and attributes + + Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + + Returns + ------ + API Gateway Lambda Proxy Output Format: dict + + Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + """ + + return { + "statusCode": 200, + "body": json.dumps( + { + "message": "hello world", + } + ), + } diff --git a/apps/sandbox/lambda-python/project.json b/apps/sandbox/lambda-python/project.json new file mode 100644 index 0000000000..43a8a0e8c7 --- /dev/null +++ b/apps/sandbox/lambda-python/project.json @@ -0,0 +1,34 @@ +{ + "name": "sandbox-lambda-python", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "command": "sam build", + "cwd": "{projectRoot}" + } + }, + "serve": { + "executor": "nx:run-commands", + "options": { + "command": "docker/sandbox/serve.sh {projectName}" + } + }, + "serve-detach": { + "executor": "nx:run-commands", + "options": { + "command": "docker/sandbox/serve-detach.sh {projectName}" + } + }, + "invoke": { + "executor": "nx:run-commands", + "options": { + "command": "curl -X POST 'http://localhost:9000/2015-03-31/functions/function/invocations' --data @{args.event}", + "cwd": "{projectRoot}" + } + } + }, + "tags": ["language:python"] +} diff --git a/apps/sandbox/lambda-python/requirements.txt b/apps/sandbox/lambda-python/requirements.txt new file mode 100644 index 0000000000..663bd1f6a2 --- /dev/null +++ b/apps/sandbox/lambda-python/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/apps/sandbox/lambda-python/samconfig.toml b/apps/sandbox/lambda-python/samconfig.toml new file mode 100644 index 0000000000..876f8787d8 --- /dev/null +++ b/apps/sandbox/lambda-python/samconfig.toml @@ -0,0 +1,30 @@ +# More information about the configuration file can be found here: +# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html +version = 0.1 + +[default.global.parameters] +stack_name = "lambda-python" + +[default.build.parameters] +parallel = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true +resolve_image_repos = true + +[default.package.parameters] +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" diff --git a/apps/sandbox/lambda-python/template.yaml b/apps/sandbox/lambda-python/template.yaml new file mode 100644 index 0000000000..e04fd5b8cd --- /dev/null +++ b/apps/sandbox/lambda-python/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + python3.13 + + Sample SAM Template for lambda-python + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + PackageType: Image + Architectures: + - x86_64 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + Metadata: + Dockerfile: Dockerfile + DockerContext: . + DockerTag: python3.13-v1 + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: 'API Gateway endpoint URL for Prod stage for Hello World function' + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/' + HelloWorldFunction: + Description: 'Hello World Lambda Function ARN' + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: 'Implicit IAM Role created for Hello World function' + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/apps/sandbox/lambda-python/tests/__init__.py b/apps/sandbox/lambda-python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/sandbox/lambda-python/tests/unit/__init__.py b/apps/sandbox/lambda-python/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/sandbox/lambda-python/tests/unit/test_handler.py b/apps/sandbox/lambda-python/tests/unit/test_handler.py new file mode 100644 index 0000000000..8787d38ed0 --- /dev/null +++ b/apps/sandbox/lambda-python/tests/unit/test_handler.py @@ -0,0 +1,72 @@ +import json + +import pytest + +from hello_world import app + + +@pytest.fixture() +def apigw_event(): + """Generates API GW Event""" + + return { + "body": '{ "test": "body"}', + "resource": "/{proxy+}", + "requestContext": { + "resourceId": "123456", + "apiId": "1234567890", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "accountId": "123456789012", + "identity": { + "apiKey": "", + "userArn": "", + "cognitoAuthenticationType": "", + "caller": "", + "userAgent": "Custom User Agent String", + "user": "", + "cognitoIdentityPoolId": "", + "cognitoIdentityId": "", + "cognitoAuthenticationProvider": "", + "sourceIp": "127.0.0.1", + "accountId": "", + }, + "stage": "prod", + }, + "queryStringParameters": {"foo": "bar"}, + "headers": { + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "Accept-Language": "en-US,en;q=0.8", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Mobile-Viewer": "false", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "CloudFront-Viewer-Country": "US", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + "X-Forwarded-Port": "443", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "X-Forwarded-Proto": "https", + "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==", + "CloudFront-Is-Tablet-Viewer": "false", + "Cache-Control": "max-age=0", + "User-Agent": "Custom User Agent String", + "CloudFront-Forwarded-Proto": "https", + "Accept-Encoding": "gzip, deflate, sdch", + }, + "pathParameters": {"proxy": "/examplepath"}, + "httpMethod": "POST", + "stageVariables": {"baz": "qux"}, + "path": "/examplepath", + } + + +def test_lambda_handler(apigw_event, mocker): + + ret = app.lambda_handler(apigw_event, "") + data = json.loads(ret["body"]) + + assert ret["statusCode"] == 200 + assert "message" in ret["body"] + assert data["message"] == "hello world" diff --git a/docker/sandbox/serve-detach.sh b/docker/sandbox/serve-detach.sh index a6bfcfd5ff..67c2cd9e5d 100755 --- a/docker/sandbox/serve-detach.sh +++ b/docker/sandbox/serve-detach.sh @@ -3,6 +3,7 @@ args=( # List of services in alphanumeric order --file docker/sandbox/services/lambda-nodejs.yml + --file docker/sandbox/services/lambda-python.yml --file docker/sandbox/networks.yml --file docker/sandbox/volumes.yml diff --git a/docker/sandbox/serve.sh b/docker/sandbox/serve.sh index d74c8d4460..4b0c3c0e4e 100755 --- a/docker/sandbox/serve.sh +++ b/docker/sandbox/serve.sh @@ -3,6 +3,7 @@ args=( # List of services in alphanumeric order --file docker/sandbox/services/lambda-nodejs.yml + --file docker/sandbox/services/lambda-python.yml --file docker/sandbox/networks.yml --file docker/sandbox/volumes.yml diff --git a/docker/sandbox/services/lambda-python.yml b/docker/sandbox/services/lambda-python.yml new file mode 100644 index 0000000000..8acfd3b569 --- /dev/null +++ b/docker/sandbox/services/lambda-python.yml @@ -0,0 +1,9 @@ +services: + sandbox-lambda-python: + image: ghcr.io/sage-bionetworks/sandbox-lambda-python:${SANDBOX_VERSION:-local} + container_name: sandbox-lambda-python + restart: always + networks: + - sandbox + ports: + - '9000:8080'