├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── biome.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── .vscode └── settings.json ├── API.md ├── LICENSE ├── README.md ├── biome.json ├── package-lock.json ├── package.json ├── resources └── Dockerfile ├── src ├── bundling.ts ├── function.ts ├── index.ts └── types.ts ├── test ├── function.test.ts └── resources │ ├── basic_app │ ├── .python-version │ ├── README.md │ ├── handler.py │ ├── pyproject.toml │ └── uv.lock │ └── workspaces_app │ ├── .python-version │ ├── README.md │ ├── app │ ├── .python-version │ ├── README.md │ ├── app_handler.py │ └── pyproject.toml │ ├── common │ ├── .python-version │ ├── README.md │ ├── pyproject.toml │ └── src │ │ └── common │ │ └── __init__.py │ ├── pyproject.toml │ └── uv.lock ├── tsconfig.dev.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/biome.yml linguist-generated 8 | /.github/workflows/build.yml linguist-generated 9 | /.github/workflows/pull-request-lint.yml linguist-generated 10 | /.github/workflows/release.yml linguist-generated 11 | /.github/workflows/upgrade-main.yml linguist-generated 12 | /.gitignore linguist-generated 13 | /.mergify.yml linguist-generated 14 | /.npmignore linguist-generated 15 | /.projen/** linguist-generated 16 | /.projen/deps.json linguist-generated 17 | /.projen/files.json linguist-generated 18 | /.projen/tasks.json linguist-generated 19 | /API.md linguist-generated 20 | /LICENSE linguist-generated 21 | /package.json linguist-generated 22 | /tsconfig.dev.json linguist-generated 23 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/biome.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: biome 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | jobs: 9 | biome: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: "20" 19 | - run: yarn install 20 | - run: npx biome check 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen build 30 | - name: Find mutations 31 | id: self_mutation 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 35 | working-directory: ./ 36 | - name: Upload patch 37 | if: steps.self_mutation.outputs.self_mutation_happened 38 | uses: actions/upload-artifact@v4.4.0 39 | with: 40 | name: repo.patch 41 | path: repo.patch 42 | overwrite: true 43 | - name: Fail build on mutation 44 | if: steps.self_mutation.outputs.self_mutation_happened 45 | run: |- 46 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 47 | cat repo.patch 48 | exit 1 49 | - name: Backup artifact permissions 50 | run: cd dist && getfacl -R . > permissions-backup.acl 51 | continue-on-error: true 52 | - name: Upload artifact 53 | uses: actions/upload-artifact@v4.4.0 54 | with: 55 | name: build-artifact 56 | path: dist 57 | overwrite: true 58 | self-mutation: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | permissions: 62 | contents: write 63 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | with: 68 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 69 | ref: ${{ github.event.pull_request.head.ref }} 70 | repository: ${{ github.event.pull_request.head.repo.full_name }} 71 | - name: Download patch 72 | uses: actions/download-artifact@v4 73 | with: 74 | name: repo.patch 75 | path: ${{ runner.temp }} 76 | - name: Apply patch 77 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 78 | - name: Set git identity 79 | run: |- 80 | git config user.name "github-actions" 81 | git config user.email "github-actions@github.com" 82 | - name: Push changes 83 | env: 84 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 85 | run: |- 86 | git add . 87 | git commit -s -m "chore: self mutation" 88 | git push origin HEAD:$PULL_REQUEST_REF 89 | package-js: 90 | needs: build 91 | runs-on: ubuntu-latest 92 | permissions: 93 | contents: read 94 | if: ${{ !needs.build.outputs.self_mutation_happened }} 95 | steps: 96 | - uses: actions/setup-node@v4 97 | with: 98 | node-version: lts/* 99 | - name: Download build artifacts 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: build-artifact 103 | path: dist 104 | - name: Restore build artifact permissions 105 | run: cd dist && setfacl --restore=permissions-backup.acl 106 | continue-on-error: true 107 | - name: Checkout 108 | uses: actions/checkout@v4 109 | with: 110 | ref: ${{ github.event.pull_request.head.ref }} 111 | repository: ${{ github.event.pull_request.head.repo.full_name }} 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && yarn install --check-files --frozen-lockfile 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | package-python: 124 | needs: build 125 | runs-on: ubuntu-latest 126 | permissions: 127 | contents: read 128 | if: ${{ !needs.build.outputs.self_mutation_happened }} 129 | steps: 130 | - uses: actions/setup-node@v4 131 | with: 132 | node-version: lts/* 133 | - uses: actions/setup-python@v5 134 | with: 135 | python-version: 3.x 136 | - name: Download build artifacts 137 | uses: actions/download-artifact@v4 138 | with: 139 | name: build-artifact 140 | path: dist 141 | - name: Restore build artifact permissions 142 | run: cd dist && setfacl --restore=permissions-backup.acl 143 | continue-on-error: true 144 | - name: Checkout 145 | uses: actions/checkout@v4 146 | with: 147 | ref: ${{ github.event.pull_request.head.ref }} 148 | repository: ${{ github.event.pull_request.head.repo.full_name }} 149 | path: .repo 150 | - name: Install Dependencies 151 | run: cd .repo && yarn install --check-files --frozen-lockfile 152 | - name: Extract build artifact 153 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 154 | - name: Move build artifact out of the way 155 | run: mv dist dist.old 156 | - name: Create python artifact 157 | run: cd .repo && npx projen package:python 158 | - name: Collect python artifact 159 | run: mv .repo/dist dist 160 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | - release_pypi 67 | runs-on: ubuntu-latest 68 | permissions: 69 | contents: write 70 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 71 | steps: 72 | - uses: actions/setup-node@v4 73 | with: 74 | node-version: lts/* 75 | - name: Download build artifacts 76 | uses: actions/download-artifact@v4 77 | with: 78 | name: build-artifact 79 | path: dist 80 | - name: Restore build artifact permissions 81 | run: cd dist && setfacl --restore=permissions-backup.acl 82 | continue-on-error: true 83 | - name: Release 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | GITHUB_REPOSITORY: ${{ github.repository }} 87 | GITHUB_REF: ${{ github.sha }} 88 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 89 | release_npm: 90 | name: Publish to npm 91 | needs: release 92 | runs-on: ubuntu-latest 93 | permissions: 94 | id-token: write 95 | contents: read 96 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 97 | steps: 98 | - uses: actions/setup-node@v4 99 | with: 100 | node-version: lts/* 101 | - name: Download build artifacts 102 | uses: actions/download-artifact@v4 103 | with: 104 | name: build-artifact 105 | path: dist 106 | - name: Restore build artifact permissions 107 | run: cd dist && setfacl --restore=permissions-backup.acl 108 | continue-on-error: true 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && yarn install --check-files --frozen-lockfile 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | - name: Release 124 | env: 125 | NPM_DIST_TAG: latest 126 | NPM_REGISTRY: registry.npmjs.org 127 | NPM_CONFIG_PROVENANCE: "true" 128 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 129 | run: npx -p publib@latest publib-npm 130 | release_pypi: 131 | name: Publish to PyPI 132 | needs: release 133 | runs-on: ubuntu-latest 134 | permissions: 135 | contents: read 136 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 137 | steps: 138 | - uses: actions/setup-node@v4 139 | with: 140 | node-version: lts/* 141 | - uses: actions/setup-python@v5 142 | with: 143 | python-version: 3.x 144 | - name: Download build artifacts 145 | uses: actions/download-artifact@v4 146 | with: 147 | name: build-artifact 148 | path: dist 149 | - name: Restore build artifact permissions 150 | run: cd dist && setfacl --restore=permissions-backup.acl 151 | continue-on-error: true 152 | - name: Checkout 153 | uses: actions/checkout@v4 154 | with: 155 | path: .repo 156 | - name: Install Dependencies 157 | run: cd .repo && yarn install --check-files --frozen-lockfile 158 | - name: Extract build artifact 159 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 160 | - name: Move build artifact out of the way 161 | run: mv dist dist.old 162 | - name: Create python artifact 163 | run: cd .repo && npx projen package:python 164 | - name: Collect python artifact 165 | run: mv .repo/dist dist 166 | - name: Release 167 | env: 168 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 169 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 170 | run: npx -p publib@latest publib-pypi 171 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "chore(deps): upgrade dependencies" 82 | body: |- 83 | Upgrades project dependencies. See details in [workflow run]. 84 | 85 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 86 | 87 | ------ 88 | 89 | *Automatically created by projen via the "upgrade-main" workflow* 90 | author: github-actions 91 | committer: github-actions 92 | signoff: true 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | /test-reports/ 34 | junit.xml 35 | /coverage/ 36 | !/.github/workflows/build.yml 37 | /dist/changelog.md 38 | /dist/version.txt 39 | !/.github/workflows/release.yml 40 | !/.mergify.yml 41 | !/.github/workflows/upgrade-main.yml 42 | !/.github/pull_request_template.md 43 | !/test/ 44 | !/tsconfig.dev.json 45 | !/src/ 46 | /lib 47 | /dist/ 48 | .jsii 49 | tsconfig.json 50 | !/API.md 51 | !/.github/workflows/biome.yml 52 | !/.projenrc.ts 53 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | - status-success=package-js 11 | - status-success=package-python 12 | merge_method: squash 13 | commit_message_template: |- 14 | {{ title }} (#{{ number }}) 15 | 16 | {{ body }} 17 | pull_request_rules: 18 | - name: Automatic merge on approval and successful build 19 | actions: 20 | delete_head_branch: {} 21 | queue: 22 | name: default 23 | conditions: 24 | - "#approved-reviews-by>=1" 25 | - -label~=(do-not-merge) 26 | - status-success=build 27 | - status-success=package-js 28 | - status-success=package-python 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | !.jsii 24 | /.gitattributes 25 | /.projenrc.ts 26 | /projenrc 27 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@biomejs/biome", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/jest", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@types/node", 13 | "type": "build" 14 | }, 15 | { 16 | "name": "commit-and-tag-version", 17 | "version": "^12", 18 | "type": "build" 19 | }, 20 | { 21 | "name": "jest", 22 | "type": "build" 23 | }, 24 | { 25 | "name": "jest-junit", 26 | "version": "^16", 27 | "type": "build" 28 | }, 29 | { 30 | "name": "jsii-diff", 31 | "type": "build" 32 | }, 33 | { 34 | "name": "jsii-docgen", 35 | "version": "^10.5.0", 36 | "type": "build" 37 | }, 38 | { 39 | "name": "jsii-pacmak", 40 | "type": "build" 41 | }, 42 | { 43 | "name": "jsii-rosetta", 44 | "version": "~5.5.0", 45 | "type": "build" 46 | }, 47 | { 48 | "name": "jsii", 49 | "version": "~5.5.0", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "projen", 54 | "type": "build" 55 | }, 56 | { 57 | "name": "ts-jest", 58 | "type": "build" 59 | }, 60 | { 61 | "name": "ts-node", 62 | "type": "build" 63 | }, 64 | { 65 | "name": "typescript", 66 | "type": "build" 67 | }, 68 | { 69 | "name": "aws-cdk-lib", 70 | "version": "^2.161.1", 71 | "type": "peer" 72 | }, 73 | { 74 | "name": "constructs", 75 | "version": "^10.3.0", 76 | "type": "peer" 77 | } 78 | ], 79 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 80 | } 81 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".gitattributes", 4 | ".github/pull_request_template.md", 5 | ".github/workflows/biome.yml", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/release.yml", 9 | ".github/workflows/upgrade-main.yml", 10 | ".gitignore", 11 | ".mergify.yml", 12 | ".projen/deps.json", 13 | ".projen/files.json", 14 | ".projen/tasks.json", 15 | "LICENSE", 16 | "tsconfig.dev.json" 17 | ], 18 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 19 | } 20 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 37 | }, 38 | "steps": [ 39 | { 40 | "builtin": "release/bump-version" 41 | } 42 | ], 43 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 44 | }, 45 | "clobber": { 46 | "name": "clobber", 47 | "description": "hard resets to HEAD of origin and cleans the local repo", 48 | "env": { 49 | "BRANCH": "$(git branch --show-current)" 50 | }, 51 | "steps": [ 52 | { 53 | "exec": "git checkout -b scratch", 54 | "name": "save current HEAD in \"scratch\" branch" 55 | }, 56 | { 57 | "exec": "git checkout $BRANCH" 58 | }, 59 | { 60 | "exec": "git fetch origin", 61 | "name": "fetch latest changes from origin" 62 | }, 63 | { 64 | "exec": "git reset --hard origin/$BRANCH", 65 | "name": "hard reset to origin commit" 66 | }, 67 | { 68 | "exec": "git clean -fdx", 69 | "name": "clean all untracked files" 70 | }, 71 | { 72 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 73 | } 74 | ], 75 | "condition": "git diff --exit-code > /dev/null" 76 | }, 77 | "compat": { 78 | "name": "compat", 79 | "description": "Perform API compatibility check against latest version", 80 | "steps": [ 81 | { 82 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)" 83 | } 84 | ] 85 | }, 86 | "compile": { 87 | "name": "compile", 88 | "description": "Only compile", 89 | "steps": [ 90 | { 91 | "exec": "jsii --silence-warnings=reserved-word" 92 | } 93 | ] 94 | }, 95 | "default": { 96 | "name": "default", 97 | "description": "Synthesize project files", 98 | "steps": [ 99 | { 100 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 101 | } 102 | ] 103 | }, 104 | "docgen": { 105 | "name": "docgen", 106 | "description": "Generate API.md from .jsii manifest", 107 | "steps": [ 108 | { 109 | "exec": "jsii-docgen -o API.md" 110 | } 111 | ] 112 | }, 113 | "eject": { 114 | "name": "eject", 115 | "description": "Remove projen from the project", 116 | "env": { 117 | "PROJEN_EJECTING": "true" 118 | }, 119 | "steps": [ 120 | { 121 | "spawn": "default" 122 | } 123 | ] 124 | }, 125 | "install": { 126 | "name": "install", 127 | "description": "Install project dependencies and update lockfile (non-frozen)", 128 | "steps": [ 129 | { 130 | "exec": "yarn install --check-files" 131 | } 132 | ] 133 | }, 134 | "install:ci": { 135 | "name": "install:ci", 136 | "description": "Install project dependencies using frozen lockfile", 137 | "steps": [ 138 | { 139 | "exec": "yarn install --check-files --frozen-lockfile" 140 | } 141 | ] 142 | }, 143 | "package": { 144 | "name": "package", 145 | "description": "Creates the distribution package", 146 | "steps": [ 147 | { 148 | "spawn": "package:js", 149 | "condition": "node -e \"if (!process.env.CI) process.exit(1)\"" 150 | }, 151 | { 152 | "spawn": "package-all", 153 | "condition": "node -e \"if (process.env.CI) process.exit(1)\"" 154 | } 155 | ] 156 | }, 157 | "package-all": { 158 | "name": "package-all", 159 | "description": "Packages artifacts for all target languages", 160 | "steps": [ 161 | { 162 | "spawn": "package:js" 163 | }, 164 | { 165 | "spawn": "package:python" 166 | } 167 | ] 168 | }, 169 | "package:js": { 170 | "name": "package:js", 171 | "description": "Create js language bindings", 172 | "steps": [ 173 | { 174 | "exec": "jsii-pacmak -v --target js" 175 | } 176 | ] 177 | }, 178 | "package:python": { 179 | "name": "package:python", 180 | "description": "Create python language bindings", 181 | "steps": [ 182 | { 183 | "exec": "jsii-pacmak -v --target python" 184 | } 185 | ] 186 | }, 187 | "post-compile": { 188 | "name": "post-compile", 189 | "description": "Runs after successful compilation", 190 | "steps": [ 191 | { 192 | "spawn": "docgen" 193 | } 194 | ] 195 | }, 196 | "post-upgrade": { 197 | "name": "post-upgrade", 198 | "description": "Runs after upgrading dependencies" 199 | }, 200 | "pre-compile": { 201 | "name": "pre-compile", 202 | "description": "Prepare the project for compilation" 203 | }, 204 | "release": { 205 | "name": "release", 206 | "description": "Prepare a release from \"main\" branch", 207 | "env": { 208 | "RELEASE": "true" 209 | }, 210 | "steps": [ 211 | { 212 | "exec": "rm -fr dist" 213 | }, 214 | { 215 | "spawn": "bump" 216 | }, 217 | { 218 | "spawn": "build" 219 | }, 220 | { 221 | "spawn": "unbump" 222 | }, 223 | { 224 | "exec": "git diff --ignore-space-at-eol --exit-code" 225 | } 226 | ] 227 | }, 228 | "test": { 229 | "name": "test", 230 | "description": "Run tests", 231 | "steps": [ 232 | { 233 | "exec": "jest --testTimeout=300000 --passWithNoTests --updateSnapshot", 234 | "receiveArgs": true 235 | } 236 | ] 237 | }, 238 | "test:watch": { 239 | "name": "test:watch", 240 | "description": "Run jest in watch mode", 241 | "steps": [ 242 | { 243 | "exec": "jest --watch" 244 | } 245 | ] 246 | }, 247 | "unbump": { 248 | "name": "unbump", 249 | "description": "Restores version to 0.0.0", 250 | "env": { 251 | "OUTFILE": "package.json", 252 | "CHANGELOG": "dist/changelog.md", 253 | "BUMPFILE": "dist/version.txt", 254 | "RELEASETAG": "dist/releasetag.txt", 255 | "RELEASE_TAG_PREFIX": "", 256 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 257 | }, 258 | "steps": [ 259 | { 260 | "builtin": "release/reset-version" 261 | } 262 | ] 263 | }, 264 | "upgrade": { 265 | "name": "upgrade", 266 | "description": "upgrade dependencies", 267 | "env": { 268 | "CI": "0" 269 | }, 270 | "steps": [ 271 | { 272 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@biomejs/biome,@types/jest,@types/node,jest,jsii-diff,jsii-pacmak,projen,ts-jest,ts-node,typescript" 273 | }, 274 | { 275 | "exec": "yarn install --check-files" 276 | }, 277 | { 278 | "exec": "yarn upgrade @biomejs/biome @types/jest @types/node commit-and-tag-version jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii projen ts-jest ts-node typescript aws-cdk-lib constructs" 279 | }, 280 | { 281 | "exec": "npx projen" 282 | }, 283 | { 284 | "spawn": "post-upgrade" 285 | } 286 | ] 287 | }, 288 | "watch": { 289 | "name": "watch", 290 | "description": "Watch & compile in the background", 291 | "steps": [ 292 | { 293 | "exec": "jsii -w --silence-warnings=reserved-word" 294 | } 295 | ] 296 | } 297 | }, 298 | "env": { 299 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 300 | }, 301 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 302 | } 303 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { awscdk } from 'projen'; 2 | import { JobPermission } from 'projen/lib/github/workflows-model'; 3 | const project = new awscdk.AwsCdkConstructLibrary({ 4 | author: 'Eoin Shanaghy', 5 | authorAddress: 'eoin.shanaghy@fourtheorem.com', 6 | cdkVersion: '2.161.1', 7 | constructsVersion: '10.3.0', 8 | defaultReleaseBranch: 'main', 9 | jsiiVersion: '~5.5.0', 10 | name: 'uv-python-lambda', 11 | projenrcTs: true, 12 | repositoryUrl: 'https://github.com/fourTheorem/uv-python-lambda', 13 | publishToPypi: { 14 | distName: 'uv-python-lambda', 15 | module: 'uv_python_lambda', 16 | }, 17 | // cdkVersion: '2.1.0', /* CDK version to use. */ 18 | // cdkDependencies: [], /* CDK dependencies of this module. */ 19 | // deps: [], /* Runtime dependencies of this module. */ 20 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 21 | devDeps: ['@biomejs/biome'] /* Build dependencies for this module. */, 22 | // packageName: undefined, /* The "name" in package.json. */ 23 | jestOptions: { 24 | extraCliOptions: ['--testTimeout=300000'], 25 | }, 26 | eslint: false, 27 | }); 28 | const biomeWorkflow = project.github?.addWorkflow('biome'); 29 | biomeWorkflow?.on({ 30 | pullRequest: { 31 | branches: ['main'], 32 | }, 33 | }); 34 | biomeWorkflow?.addJobs({ 35 | biome: { 36 | runsOn: ['ubuntu-latest'], 37 | permissions: { 38 | contents: JobPermission.READ, 39 | idToken: JobPermission.WRITE, 40 | }, 41 | steps: [ 42 | { 43 | uses: 'actions/checkout@v4', 44 | }, 45 | { 46 | uses: 'actions/setup-node@v4', 47 | with: { 48 | 'node-version': '20', 49 | }, 50 | }, 51 | { 52 | run: 'yarn install', 53 | }, 54 | { 55 | run: 'npx biome check', 56 | }, 57 | ], 58 | }, 59 | }); 60 | 61 | project.files; 62 | project.synth(); 63 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "eslint.enable": false, 4 | "editor.defaultFormatter": "biomejs.biome", 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uv-python-lambda 2 | 3 | CDK Construct for Python Lambda Functions using [uv](https://docs.astral.sh/uv/) 4 | 5 | ## Goals 6 | 7 | - ⚡️ Package and deploy Lambda Functions faster with `uv`'s speed 8 | - 📦 Support workspaces in a monorepo with [uv workspaces](https://docs.astral.sh/uv/concepts/workspaces/) 9 | 10 | `uv-python-lambda` is based on [aws-lambda-python-alpha](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html) with some differences: 11 | 12 | - It only supports `uv` for packaging - there is no Poetry or pip support 13 | - It supports workspaces so you can build multiple Lambda functions from different uv workspaces and have their dependencies included correctly. This is useful for, but not limited to, monorepos. 14 | 15 | ## API 16 | 17 | See [API.md](API.md) 18 | 19 | ## Example 20 | 21 | ```python 22 | from uv_python_lambda import PythonFunction 23 | from constructs import Construct 24 | 25 | # The root path should be relative to your CDK source file 26 | root_path = Path(__file__).parent.parent.parent 27 | 28 | 29 | class CdkStack(Stack): 30 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 31 | super().__init__(scope, construct_id, **kwargs) 32 | 33 | fn = PythonFunction( 34 | self, 35 | "fn", 36 | root_dir=str(root_path), 37 | index="fetcher_lambda.py", 38 | workspace_package="fetcher", # Use a workspace package as the top-level Lambda entry point. 39 | handler="handle_event", 40 | bundling={ 41 | "asset_excludes": [ 42 | ".venv/", 43 | "node_modules/", 44 | "cdk/", 45 | ".git/", 46 | ".idea/", 47 | "dist/", 48 | ] 49 | }, 50 | timeout=Duration.seconds(30), 51 | ) 52 | ``` -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [ 11 | ".projen/", 12 | "tsconfig.*.json", 13 | ".eslintrc.json", 14 | "package.json", 15 | "package-lock.json" 16 | ] 17 | }, 18 | "formatter": { 19 | "enabled": true, 20 | "indentStyle": "space", 21 | "indentWidth": 2 22 | }, 23 | "organizeImports": { 24 | "enabled": true 25 | }, 26 | "linter": { 27 | "enabled": true, 28 | "rules": { 29 | "recommended": true 30 | } 31 | }, 32 | "javascript": { 33 | "formatter": { 34 | "quoteStyle": "single" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uv-python-lambda", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/fourTheorem/uv-python-lambda" 6 | }, 7 | "scripts": { 8 | "build": "npx projen build", 9 | "bump": "npx projen bump", 10 | "clobber": "npx projen clobber", 11 | "compat": "npx projen compat", 12 | "compile": "npx projen compile", 13 | "default": "npx projen default", 14 | "docgen": "npx projen docgen", 15 | "eject": "npx projen eject", 16 | "package": "npx projen package", 17 | "package-all": "npx projen package-all", 18 | "package:js": "npx projen package:js", 19 | "package:python": "npx projen package:python", 20 | "post-compile": "npx projen post-compile", 21 | "post-upgrade": "npx projen post-upgrade", 22 | "pre-compile": "npx projen pre-compile", 23 | "release": "npx projen release", 24 | "test": "npx projen test", 25 | "test:watch": "npx projen test:watch", 26 | "unbump": "npx projen unbump", 27 | "upgrade": "npx projen upgrade", 28 | "watch": "npx projen watch", 29 | "projen": "npx projen" 30 | }, 31 | "author": { 32 | "name": "Eoin Shanaghy", 33 | "email": "eoin.shanaghy@fourtheorem.com", 34 | "organization": false 35 | }, 36 | "devDependencies": { 37 | "@biomejs/biome": "^1.9.4", 38 | "@types/jest": "^29.5.14", 39 | "@types/node": "^22.13.1", 40 | "aws-cdk-lib": "2.161.1", 41 | "commit-and-tag-version": "^12", 42 | "constructs": "10.3.0", 43 | "jest": "^29.7.0", 44 | "jest-junit": "^16", 45 | "jsii": "~5.5.0", 46 | "jsii-diff": "^1.106.0", 47 | "jsii-docgen": "^10.5.0", 48 | "jsii-pacmak": "^1.106.0", 49 | "jsii-rosetta": "~5.5.0", 50 | "projen": "^0.91.7", 51 | "ts-jest": "^29.2.5", 52 | "ts-node": "^10.9.2", 53 | "typescript": "^5.7.3" 54 | }, 55 | "peerDependencies": { 56 | "aws-cdk-lib": "^2.161.1", 57 | "constructs": "^10.3.0" 58 | }, 59 | "keywords": [ 60 | "cdk" 61 | ], 62 | "main": "lib/index.js", 63 | "license": "Apache-2.0", 64 | "publishConfig": { 65 | "access": "public" 66 | }, 67 | "typesVersions": { 68 | "<=3.9": { 69 | "lib/*": [ 70 | "lib/.types-compat/ts3.9/*", 71 | "lib/.types-compat/ts3.9/*/index.d.ts" 72 | ] 73 | } 74 | }, 75 | "version": "0.0.0", 76 | "jest": { 77 | "coverageProvider": "v8", 78 | "testMatch": [ 79 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 80 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 81 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 82 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 83 | ], 84 | "clearMocks": true, 85 | "collectCoverage": true, 86 | "coverageReporters": [ 87 | "json", 88 | "lcov", 89 | "clover", 90 | "cobertura", 91 | "text" 92 | ], 93 | "coverageDirectory": "coverage", 94 | "coveragePathIgnorePatterns": [ 95 | "/node_modules/" 96 | ], 97 | "testPathIgnorePatterns": [ 98 | "/node_modules/" 99 | ], 100 | "watchPathIgnorePatterns": [ 101 | "/node_modules/" 102 | ], 103 | "reporters": [ 104 | "default", 105 | [ 106 | "jest-junit", 107 | { 108 | "outputDirectory": "test-reports" 109 | } 110 | ] 111 | ], 112 | "transform": { 113 | "^.+\\.[t]sx?$": [ 114 | "ts-jest", 115 | { 116 | "tsconfig": "tsconfig.dev.json" 117 | } 118 | ] 119 | } 120 | }, 121 | "types": "lib/index.d.ts", 122 | "stability": "stable", 123 | "jsii": { 124 | "outdir": "dist", 125 | "targets": { 126 | "python": { 127 | "distName": "uv-python-lambda", 128 | "module": "uv_python_lambda" 129 | } 130 | }, 131 | "tsc": { 132 | "outDir": "lib", 133 | "rootDir": "src" 134 | } 135 | }, 136 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 137 | } 138 | -------------------------------------------------------------------------------- /resources/Dockerfile: -------------------------------------------------------------------------------- 1 | # From https://github.com/aws/aws-cdk/blob/95f8cef0505dd2deb8ee5e45ab98c6ab1b764b02/packages/%40aws-cdk/aws-lambda-python-alpha/lib/Dockerfile 2 | # The correct AWS SAM build image based on the runtime of the function will be 3 | # passed as build arg. The default allows to do `docker build .` when testing. 4 | ARG PYTHON_VERSION=3.7 5 | ARG IMAGE=public.ecr.aws/sam/build-python${PYTHON_VERSION} 6 | FROM $IMAGE 7 | 8 | ARG PIP_INDEX_URL 9 | ARG PIP_EXTRA_INDEX_URL 10 | ARG HTTPS_PROXY 11 | ARG UV_VERSION=0.4.20 12 | 13 | ENV PIP_CACHE_DIR=/tmp/pip-cache 14 | ENV UV_CACHE_DIR=/tmp/uv-cache 15 | 16 | RUN mkdir /tmp/pip-cache && \ 17 | chmod -R 777 /tmp/pip-cache && \ 18 | pip install uv==$UV_VERSION && \ 19 | rm -rf /tmp/pip-cache/* 20 | 21 | CMD [ "python" ] 22 | -------------------------------------------------------------------------------- /src/bundling.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { 3 | AssetHashType, 4 | AssetStaging, 5 | type BundlingFileAccess, 6 | DockerImage, 7 | type DockerVolume, 8 | } from 'aws-cdk-lib'; 9 | import { 10 | Architecture, 11 | type AssetCode, 12 | Code, 13 | type Runtime, 14 | } from 'aws-cdk-lib/aws-lambda'; 15 | import type { BundlingOptions, ICommandHooks } from './types'; 16 | 17 | export const HASHABLE_DEPENDENCIES_EXCLUDE = [ 18 | '*.pyc', 19 | 'cdk/**', 20 | '.git/**', 21 | '.venv/**', 22 | ]; 23 | 24 | export const DEFAULT_ASSET_EXCLUDES = [ 25 | '.venv/', 26 | 'node_modules/', 27 | 'cdk.out/', 28 | '.git/', 29 | 'cdk', 30 | ]; 31 | 32 | interface BundlingCommandOptions { 33 | readonly rootDir: string; 34 | readonly workspacePackage?: string; 35 | readonly inputDir: string; 36 | readonly outputDir: string; 37 | readonly assetExcludes: string[]; 38 | readonly commandHooks?: ICommandHooks; 39 | } 40 | 41 | export interface BundlingProps extends BundlingOptions { 42 | /** 43 | * uv project root (workspace root) 44 | */ 45 | readonly rootDir: string; 46 | 47 | /** 48 | * uv package to use for the Lambda Function 49 | */ 50 | readonly workspacePackage?: string; 51 | 52 | /** 53 | * Lambda runtime (must be one of the Python runtimes) 54 | */ 55 | readonly runtime: Runtime; 56 | 57 | /** 58 | * Lambda CPU architecture 59 | * 60 | * @default Architecture.ARM_64 61 | */ 62 | readonly architecture?: Architecture; 63 | 64 | /** 65 | * Skip bundling process 66 | * 67 | * @default false 68 | */ 69 | readonly skip?: boolean; 70 | 71 | /** 72 | * Glob patterns to exclude from asset hash fingerprinting used for source change 73 | * detection 74 | * 75 | * @default HASHABLE_DEPENDENCIES_EXCLUDE 76 | */ 77 | readonly hashableAssetExclude?: string[]; 78 | } 79 | 80 | /** 81 | * Bundling options for Python Lambda assets 82 | */ 83 | export class Bundling { 84 | public static bundle(options: BundlingProps): AssetCode { 85 | const { 86 | hashableAssetExclude = HASHABLE_DEPENDENCIES_EXCLUDE, 87 | ...bundlingOptions 88 | } = options; 89 | return Code.fromAsset(options.rootDir, { 90 | assetHashType: AssetHashType.SOURCE, 91 | exclude: hashableAssetExclude, 92 | bundling: new Bundling(bundlingOptions), 93 | }); 94 | } 95 | 96 | public readonly image: DockerImage; 97 | public readonly entrypoint?: string[] | undefined; 98 | public readonly command: string[] | undefined; 99 | public readonly volumes?: DockerVolume[] | undefined; 100 | public readonly volumesFrom?: string[] | undefined; 101 | public readonly environment?: { [key: string]: string } | undefined; 102 | public readonly workingDirectory?: string | undefined; 103 | public readonly user?: string | undefined; 104 | public readonly securityOpt?: string | undefined; 105 | public readonly network?: string | undefined; 106 | public readonly bundlingFileAccess?: BundlingFileAccess | undefined; 107 | 108 | constructor(props: BundlingProps) { 109 | const { 110 | rootDir, 111 | workspacePackage, 112 | image, 113 | commandHooks, 114 | assetExcludes = DEFAULT_ASSET_EXCLUDES, 115 | } = props; 116 | 117 | const bundlingCommands = props.skip 118 | ? [] 119 | : this.createBundlingCommands({ 120 | rootDir, 121 | workspacePackage, 122 | assetExcludes, 123 | commandHooks, 124 | inputDir: AssetStaging.BUNDLING_INPUT_DIR, 125 | outputDir: AssetStaging.BUNDLING_OUTPUT_DIR, 126 | }); 127 | 128 | this.image = image ?? this.createDockerImage(props); 129 | 130 | this.command = props.command ?? [ 131 | 'bash', 132 | '-c', 133 | bundlingCommands.join(' && '), 134 | ]; 135 | this.entrypoint = props.entrypoint; 136 | this.volumes = props.volumes; 137 | this.volumesFrom = props.volumesFrom; 138 | this.environment = props.environment; 139 | this.workingDirectory = props.workingDirectory; 140 | this.user = props.user; 141 | this.securityOpt = props.securityOpt; 142 | this.network = props.network; 143 | this.bundlingFileAccess = props.bundlingFileAccess; 144 | } 145 | 146 | private createDockerImage(props: BundlingProps): DockerImage { 147 | // If skip is true then don't call DockerImage.fromBuild as that calls dockerExec. 148 | // Return a dummy object of the right type as it's not going to be used. 149 | if (props.skip) { 150 | return new DockerImage('skipped'); 151 | } 152 | 153 | return DockerImage.fromBuild(path.resolve(__dirname, '..', 'resources'), { 154 | buildArgs: { 155 | ...props.buildArgs, 156 | IMAGE: props.runtime.bundlingImage.image, 157 | }, 158 | platform: (props.architecture ?? Architecture.ARM_64).dockerPlatform, 159 | }); 160 | } 161 | 162 | private createBundlingCommands(options: BundlingCommandOptions): string[] { 163 | const excludeArgs = options.assetExcludes.map( 164 | (exclude) => `--exclude="${exclude}"`, 165 | ); 166 | const workspacePackage = options.workspacePackage; 167 | const uvCommonArgs = `--directory ${options.outputDir}`; 168 | const uvPackageArgs = workspacePackage 169 | ? `--package ${workspacePackage}` 170 | : ''; 171 | const reqsFile = `/tmp/requirements${workspacePackage || ''}.txt`; 172 | const commands = []; 173 | commands.push( 174 | ...(options.commandHooks?.beforeBundling( 175 | options.inputDir, 176 | options.outputDir, 177 | ) ?? []), 178 | ); 179 | commands.push( 180 | ...[ 181 | `rsync -rLv ${excludeArgs.join(' ')} ${options.inputDir}/ ${options.outputDir}`, 182 | `cd ${options.outputDir}`, // uv pip install needs to be run from here for editable deps to relative paths to be resolved 183 | `uv sync ${uvCommonArgs} ${uvPackageArgs} --python-preference=only-system --compile-bytecode --no-dev --frozen --no-editable --link-mode=copy`, 184 | `uv export ${uvCommonArgs} ${uvPackageArgs} --no-dev --frozen --no-editable > ${reqsFile}`, 185 | `uv pip install -r ${reqsFile} --target ${options.outputDir} --reinstall --compile-bytecode --link-mode=copy`, 186 | `rm -rf ${options.outputDir}/.venv`, 187 | ], 188 | ); 189 | commands.push( 190 | ...(options.commandHooks?.afterBundling( 191 | options.inputDir, 192 | options.outputDir, 193 | ) ?? []), 194 | ); 195 | 196 | return commands; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/function.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { Stack } from 'aws-cdk-lib'; 4 | import { 5 | Architecture, 6 | type CfnFunction, 7 | // biome-ignore lint/suspicious/noShadowRestrictedNames: shadows 'function' 8 | Function, 9 | type FunctionOptions, 10 | Runtime, 11 | RuntimeFamily, 12 | } from 'aws-cdk-lib/aws-lambda'; 13 | 14 | import type { Construct } from 'constructs'; 15 | import { Bundling } from './bundling'; 16 | import type { BundlingOptions } from './types'; 17 | 18 | export interface PythonFunctionProps extends FunctionOptions { 19 | /** 20 | * UV project root directory (workspace root) 21 | */ 22 | readonly rootDir: string; 23 | 24 | /** 25 | * Optional UV project workspace, used to specify a specific package to be used 26 | * as a Lambda Function entry. 27 | */ 28 | readonly workspacePackage?: string; 29 | 30 | /** 31 | * The runtime 32 | * 33 | * @default Runtime.PYTHON_3_12 34 | */ 35 | readonly runtime?: Runtime; 36 | 37 | /** 38 | * The path to the index file with the project or (or workspace, if specified) containing the handler. 39 | * 40 | * @default index.py 41 | */ 42 | readonly index?: string; 43 | 44 | /** 45 | * The name of the exported handler function in the #index 46 | * 47 | * @default handler 48 | */ 49 | readonly handler?: string; 50 | 51 | /** 52 | * Custom bundling options, including build architecture and bundling container image 53 | */ 54 | readonly bundling?: BundlingOptions; 55 | } 56 | 57 | export class PythonFunction extends Function { 58 | constructor(scope: Construct, id: string, props: PythonFunctionProps) { 59 | const { 60 | workspacePackage, 61 | handler = 'handler', 62 | index = 'index.py', 63 | runtime = Runtime.PYTHON_3_12, 64 | } = props; 65 | 66 | const architecture = props.architecture ?? Architecture.ARM_64; 67 | const rootDir = path.resolve(props.rootDir); 68 | 69 | // Strip .py from the end of handler if it exists 70 | const strippedIndex = index.endsWith('.py') ? index.slice(0, -3) : index; 71 | 72 | const resolvedHandler = `${strippedIndex}.${handler}`.replace(/\//g, '.'); 73 | 74 | if (runtime.family !== RuntimeFamily.PYTHON) { 75 | throw new Error('Only Python runtimes are supported'); 76 | } 77 | 78 | const skip = !Stack.of(scope).bundlingRequired; 79 | 80 | const code = Bundling.bundle({ 81 | rootDir, 82 | runtime, 83 | skip: skip, 84 | architecture, 85 | workspacePackage, 86 | ...props.bundling, 87 | }); 88 | 89 | const environment = props.environment ?? {}; 90 | 91 | super(scope, id, { 92 | ...props, 93 | environment, 94 | architecture, 95 | runtime, 96 | code, 97 | handler: resolvedHandler, 98 | }); 99 | 100 | if (skip) { 101 | return; 102 | } 103 | 104 | const assetPath = (this.node.defaultChild as CfnFunction).getMetadata( 105 | 'aws:asset:path', 106 | ); 107 | if (!assetPath) { 108 | return; 109 | } 110 | 111 | const codePath = path.join(process.env.CDK_OUTDIR as string, assetPath); 112 | const pythonPaths = getPthFilePaths(codePath); 113 | 114 | if (pythonPaths.length > 0) { 115 | let pythonPathValue = environment.PYTHONPATH; 116 | const addedPaths = pythonPaths.join(':'); 117 | pythonPathValue = pythonPathValue 118 | ? `${pythonPathValue}:${addedPaths}` 119 | : addedPaths; 120 | this.addEnvironment('PYTHONPATH', pythonPathValue); 121 | } 122 | } 123 | } 124 | 125 | function getPthFilePaths(basePath: string): string[] { 126 | const pthFiles = fs 127 | .readdirSync(basePath) 128 | .filter((file) => file.endsWith('.pth')); 129 | const pythonPaths: string[] = []; 130 | for (const pthFile of pthFiles) { 131 | const filePath = path.join(basePath, pthFile); 132 | const content = fs.readFileSync(filePath, 'utf-8'); 133 | const dirs = content.split('\n').filter((line) => line.trim() !== ''); 134 | pythonPaths.push( 135 | ...dirs.map((dir) => 136 | path.join('/var/task', path.relative('/asset-output', dir)), 137 | ), 138 | ); 139 | } 140 | return pythonPaths; 141 | } 142 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bundling'; 2 | export * from './function'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AssetHashType, 3 | BundlingFileAccess, 4 | DockerImage, 5 | DockerRunOptions, 6 | } from 'aws-cdk-lib/core'; 7 | 8 | /** 9 | * Options for bundling 10 | */ 11 | export interface BundlingOptions extends DockerRunOptions { 12 | /** 13 | * List of file patterns to exclude when copying assets from source for bundling. 14 | * 15 | * @default - DEFAULT_ASSET_EXCLUDES 16 | */ 17 | readonly assetExcludes?: string[]; 18 | 19 | /** 20 | * Output path suffix: the suffix for the directory into which the bundled output is written. 21 | * 22 | * @default - 'python' for a layer, empty string otherwise. 23 | */ 24 | readonly outputPathSuffix?: string; 25 | 26 | /** 27 | * Docker image to use for bundling. If no options are provided, the default bundling image 28 | * will be used. Dependencies will be installed using the default packaging commands 29 | * and copied over from into the Lambda asset. 30 | * 31 | * @default - Default bundling image. 32 | */ 33 | readonly image?: DockerImage; 34 | 35 | /** 36 | * Optional build arguments to pass to the default container. This can be used to customize 37 | * the index URLs used for installing dependencies. 38 | * This is not used if a custom image is provided. 39 | * 40 | * @default - No build arguments. 41 | */ 42 | readonly buildArgs?: { [key: string]: string }; 43 | 44 | /** 45 | * Determines how asset hash is calculated. Assets will get rebuild and 46 | * uploaded only if their hash has changed. 47 | * 48 | * If asset hash is set to `SOURCE` (default), then only changes to the source 49 | * directory will cause the asset to rebuild. This means, for example, that in 50 | * order to pick up a new dependency version, a change must be made to the 51 | * source tree. Ideally, this can be implemented by including a dependency 52 | * lockfile in your source tree or using fixed dependencies. 53 | * 54 | * If the asset hash is set to `OUTPUT`, the hash is calculated after 55 | * bundling. This means that any change in the output will cause the asset to 56 | * be invalidated and uploaded. Bear in mind that `pip` adds timestamps to 57 | * dependencies it installs, which implies that in this mode Python bundles 58 | * will _always_ get rebuild and uploaded. Normally this is an anti-pattern 59 | * since build 60 | * 61 | * @default AssetHashType.SOURCE By default, hash is calculated based on the 62 | * contents of the source directory. This means that only updates to the 63 | * source will cause the asset to rebuild. 64 | */ 65 | 66 | readonly assetHashType?: AssetHashType; 67 | 68 | /** 69 | * Specify a custom hash for this asset. If `assetHashType` is set it must 70 | * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will 71 | * be SHA256 hashed and encoded as hex. The resulting hash will be the asset 72 | * hash. 73 | * 74 | * NOTE: the hash is used in order to identify a specific revision of the asset, and 75 | * used for optimizing and caching deployment activities related to this asset such as 76 | * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will 77 | * need to make sure it is updated every time the asset changes, or otherwise it is 78 | * possible that some deployments will not be invalidated. 79 | * 80 | * @default - Based on `assetHashType` 81 | */ 82 | readonly assetHash?: string; 83 | 84 | /** 85 | * Command hooks 86 | * 87 | * @default - do not run additional commands 88 | */ 89 | readonly commandHooks?: ICommandHooks; 90 | 91 | /** 92 | * Which option to use to copy the source files to the docker container and output files back 93 | * @default - BundlingFileAccess.BIND_MOUNT 94 | */ 95 | readonly bundlingFileAccess?: BundlingFileAccess; 96 | } 97 | 98 | /** 99 | * Command hooks 100 | * 101 | * These commands will run in the environment in which bundling occurs: inside 102 | * the container for Docker bundling or on the host OS for local bundling. 103 | * 104 | * Commands are chained with `&&`. 105 | * 106 | * ```text 107 | * { 108 | * // Run tests prior to bundling 109 | * beforeBundling(inputDir: string, outputDir: string): string[] { 110 | * return [`pytest`]; 111 | * } 112 | * // ... 113 | * } 114 | * ``` 115 | */ 116 | export interface ICommandHooks { 117 | /** 118 | * Returns commands to run before bundling. 119 | * 120 | * Commands are chained with `&&`. 121 | */ 122 | beforeBundling(inputDir: string, outputDir: string): string[]; 123 | 124 | /** 125 | * Returns commands to run after bundling. 126 | * 127 | * Commands are chained with `&&`. 128 | */ 129 | afterBundling(inputDir: string, outputDir: string): string[]; 130 | } 131 | -------------------------------------------------------------------------------- /test/function.test.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process'; 2 | import * as fs from 'node:fs/promises'; 3 | import * as os from 'node:os'; 4 | import * as path from 'node:path'; 5 | import { promisify } from 'node:util'; 6 | import { App, Stack } from 'aws-cdk-lib'; 7 | import { Match, Template } from 'aws-cdk-lib/assertions'; 8 | import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; 9 | import * as cxapi from 'aws-cdk-lib/cx-api'; 10 | import { PythonFunction } from '../src'; 11 | const execAsync = promisify(exec); 12 | 13 | const resourcesPath = path.resolve(__dirname, 'resources'); 14 | 15 | /** 16 | * Determine the optimal Lambda Function architecture based on the Docker host's CPU 17 | * architecture. This allows GHA runners to work without slow QEMU Arm emulation. 18 | * 19 | * @returns The Lambda Architecture 20 | */ 21 | async function getDockerHostArch(): Promise { 22 | try { 23 | const { stdout } = await execAsync( 24 | 'docker info --format "{{.Architecture}}"', 25 | ); 26 | const arch = stdout.trim(); 27 | return arch === 'aarch64' ? Architecture.ARM_64 : Architecture.X86_64; 28 | } catch (error) { 29 | console.error('Error getting Docker host architecture:', error); 30 | throw error; 31 | } 32 | } 33 | 34 | /** 35 | * Create a new CDK App and Stack with the given name and set the context to ensure 36 | * that the 'aws:asset:path' metadata is set. 37 | * 38 | * @returns The App and Stack 39 | */ 40 | async function createStack(name = 'test'): Promise<{ app: App; stack: Stack }> { 41 | const app = new App({}); 42 | const stack = new Stack(app, name); 43 | 44 | // This ensures that the 'aws:asset:path' metadata is set 45 | stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); 46 | 47 | return { app, stack }; 48 | } 49 | 50 | // Need to have CDK_OUTDIR set to something sensible as it's used to create the codePath when aws:asset:path is set 51 | const OLD_ENV = process.env; 52 | 53 | beforeEach(async () => { 54 | jest.resetModules(); 55 | process.env = { ...OLD_ENV }; 56 | process.env.CDK_OUTDIR = await fs.mkdtemp( 57 | path.join(os.tmpdir(), 'uv-python-lambda-test-'), 58 | ); 59 | }); 60 | 61 | afterEach(async () => { 62 | if (process.env.CDK_OUTDIR) { 63 | await fs.rm(process.env.CDK_OUTDIR, { recursive: true }); 64 | } 65 | process.env = OLD_ENV; 66 | }); 67 | 68 | test('Create a function from basic_app', async () => { 69 | const { app, stack } = await createStack(); 70 | 71 | new PythonFunction(stack, 'basic_app', { 72 | rootDir: path.join(resourcesPath, 'basic_app'), 73 | index: 'handler.py', 74 | handler: 'lambda_handler', 75 | runtime: Runtime.PYTHON_3_12, 76 | architecture: await getDockerHostArch(), 77 | }); 78 | 79 | const template = Template.fromStack(stack); 80 | 81 | template.hasResourceProperties('AWS::Lambda::Function', { 82 | Handler: 'handler.lambda_handler', 83 | Runtime: 'python3.12', 84 | Code: { 85 | S3Bucket: Match.anyValue(), 86 | S3Key: Match.anyValue(), 87 | }, 88 | }); 89 | const functions = Object.values( 90 | template.findResources('AWS::Lambda::Function'), 91 | ); 92 | expect(functions).toHaveLength(1); 93 | const contents = await getFunctionAssetContents(functions[0], app); 94 | expect(contents).toContain('handler.py'); 95 | }); 96 | 97 | test('Create a function from basic_app with no .py index extension', async () => { 98 | const { stack } = await createStack(); 99 | 100 | new PythonFunction(stack, 'basic_app', { 101 | rootDir: path.join(resourcesPath, 'basic_app'), 102 | index: 'handler', 103 | handler: 'lambda_handler', 104 | runtime: Runtime.PYTHON_3_12, 105 | architecture: await getDockerHostArch(), 106 | }); 107 | 108 | const template = Template.fromStack(stack); 109 | 110 | template.hasResourceProperties('AWS::Lambda::Function', { 111 | Handler: 'handler.lambda_handler', 112 | Runtime: 'python3.12', 113 | Code: { 114 | S3Bucket: Match.anyValue(), 115 | S3Key: Match.anyValue(), 116 | }, 117 | }); 118 | }); 119 | 120 | test('Create a function from basic_app when skip is true', async () => { 121 | const { stack } = await createStack(); 122 | 123 | const bundlingSpy = jest 124 | .spyOn(stack, 'bundlingRequired', 'get') 125 | .mockReturnValue(false); 126 | const architecture = await getDockerHostArch(); 127 | 128 | // To see this fail, comment out the `if (skip) { return; } code in the PythonFunction constructor 129 | expect(() => { 130 | new PythonFunction(stack, 'basic_app', { 131 | rootDir: path.join(resourcesPath, 'basic_app'), 132 | index: 'handler', 133 | handler: 'lambda_handler', 134 | runtime: Runtime.PYTHON_3_12, 135 | architecture, 136 | }); 137 | }).not.toThrow(); 138 | 139 | bundlingSpy.mockRestore(); 140 | }); 141 | 142 | test('Create a function with workspaces_app', async () => { 143 | const { app, stack } = await createStack('wstest'); 144 | 145 | new PythonFunction(stack, 'workspaces_app', { 146 | rootDir: path.join(resourcesPath, 'workspaces_app'), 147 | workspacePackage: 'app', 148 | index: 'app_handler.py', 149 | handler: 'handle_event', 150 | runtime: Runtime.PYTHON_3_10, 151 | architecture: await getDockerHostArch(), 152 | }); 153 | 154 | const template = Template.fromStack(stack); 155 | 156 | template.hasResourceProperties('AWS::Lambda::Function', { 157 | Handler: 'app_handler.handle_event', 158 | Runtime: 'python3.10', 159 | Code: { 160 | S3Bucket: Match.anyValue(), 161 | S3Key: Match.anyValue(), 162 | }, 163 | }); 164 | 165 | const functions = Object.values( 166 | template.findResources('AWS::Lambda::Function'), 167 | ); 168 | expect(functions).toHaveLength(1); 169 | const contents = await getFunctionAssetContents(functions[0], app); 170 | for (const entry of [ 171 | 'app', 172 | 'common', 173 | 'pydantic', 174 | 'httpx', 175 | '_common.pth', 176 | 'app_handler.py', 177 | ]) { 178 | expect(contents).toContain(entry); 179 | } 180 | }); 181 | 182 | // biome-ignore lint/suspicious/noExplicitAny: 183 | async function getFunctionAssetContents(functionResource: any, app: App) { 184 | const [assetHash] = functionResource.Properties.Code.S3Key.split('.'); 185 | const assetPath = path.join(app.outdir, `asset.${assetHash}`); 186 | const contents = await fs.readdir(assetPath); 187 | return contents; 188 | } 189 | -------------------------------------------------------------------------------- /test/resources/basic_app/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /test/resources/basic_app/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fourTheorem/uv-python-lambda/272fe7ac5bd15e5e8dc617f2354828de6c41a879/test/resources/basic_app/README.md -------------------------------------------------------------------------------- /test/resources/basic_app/handler.py: -------------------------------------------------------------------------------- 1 | def lambda_handler(event, context): 2 | return { 3 | 'statusCode': 200, 4 | 'body': 'Hello from Lambda!' 5 | } 6 | -------------------------------------------------------------------------------- /test/resources/basic_app/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "basic-app" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [] 8 | -------------------------------------------------------------------------------- /test/resources/basic_app/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "basic-app" 6 | version = "0.1.0" 7 | source = { virtual = "." } 8 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fourTheorem/uv-python-lambda/272fe7ac5bd15e5e8dc617f2354828de6c41a879/test/resources/workspaces_app/README.md -------------------------------------------------------------------------------- /test/resources/workspaces_app/app/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/app/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fourTheorem/uv-python-lambda/272fe7ac5bd15e5e8dc617f2354828de6c41a879/test/resources/workspaces_app/app/README.md -------------------------------------------------------------------------------- /test/resources/workspaces_app/app/app_handler.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class Car(BaseModel): 4 | brand: str 5 | model: str 6 | year: int 7 | 8 | def handle_event(event, context): 9 | car = Car(brand="Toyota", model="Corolla", year=2020) 10 | return car.model_dump() -------------------------------------------------------------------------------- /test/resources/workspaces_app/app/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "common", 9 | "pydantic>=2.9.2", 10 | ] 11 | 12 | [tool.uv.sources] 13 | common = { workspace = true } 14 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/common/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/common/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fourTheorem/uv-python-lambda/272fe7ac5bd15e5e8dc617f2354828de6c41a879/test/resources/workspaces_app/common/README.md -------------------------------------------------------------------------------- /test/resources/workspaces_app/common/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "common" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "httpx>=0.27.2", 9 | ] 10 | 11 | [project.scripts] 12 | common = "common:main" 13 | 14 | [build-system] 15 | requires = ["hatchling"] 16 | build-backend = "hatchling.build" 17 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/common/src/common/__init__.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | def make_request(): 4 | response = httpx.get("https://jsonplaceholder.typicode.com/todos/1") 5 | response.raise_for_status() 6 | return response.json() -------------------------------------------------------------------------------- /test/resources/workspaces_app/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "workspaces-app" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [tool.uv.workspace] 10 | members = ["common", "app"] 11 | 12 | [tool.uv.sources] 13 | common = { workspace = true } 14 | app = { workspace = true } 15 | 16 | [build-system] 17 | requires = ["hatchling"] 18 | build-backend = "hatchling.build" 19 | 20 | [tool.hatch.build.targets.wheel] 21 | packages = ["app", "common"] 22 | -------------------------------------------------------------------------------- /test/resources/workspaces_app/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.10" 3 | resolution-markers = [ 4 | "python_full_version < '3.13'", 5 | "python_full_version >= '3.13'", 6 | ] 7 | 8 | [manifest] 9 | members = [ 10 | "app", 11 | "common", 12 | "workspaces-app", 13 | ] 14 | 15 | [[package]] 16 | name = "annotated-types" 17 | version = "0.7.0" 18 | source = { registry = "https://pypi.org/simple" } 19 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 20 | wheels = [ 21 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 22 | ] 23 | 24 | [[package]] 25 | name = "anyio" 26 | version = "4.6.2.post1" 27 | source = { registry = "https://pypi.org/simple" } 28 | dependencies = [ 29 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 30 | { name = "idna" }, 31 | { name = "sniffio" }, 32 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 33 | ] 34 | sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } 35 | wheels = [ 36 | { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, 37 | ] 38 | 39 | [[package]] 40 | name = "app" 41 | version = "0.1.0" 42 | source = { editable = "app" } 43 | dependencies = [ 44 | { name = "common" }, 45 | { name = "pydantic" }, 46 | ] 47 | 48 | [package.metadata] 49 | requires-dist = [ 50 | { name = "common", editable = "common" }, 51 | { name = "pydantic", specifier = ">=2.9.2" }, 52 | ] 53 | 54 | [[package]] 55 | name = "certifi" 56 | version = "2024.8.30" 57 | source = { registry = "https://pypi.org/simple" } 58 | sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } 59 | wheels = [ 60 | { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, 61 | ] 62 | 63 | [[package]] 64 | name = "common" 65 | version = "0.1.0" 66 | source = { editable = "common" } 67 | dependencies = [ 68 | { name = "httpx" }, 69 | ] 70 | 71 | [package.metadata] 72 | requires-dist = [{ name = "httpx", specifier = ">=0.27.2" }] 73 | 74 | [[package]] 75 | name = "exceptiongroup" 76 | version = "1.2.2" 77 | source = { registry = "https://pypi.org/simple" } 78 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 79 | wheels = [ 80 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 81 | ] 82 | 83 | [[package]] 84 | name = "h11" 85 | version = "0.14.0" 86 | source = { registry = "https://pypi.org/simple" } 87 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 88 | wheels = [ 89 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 90 | ] 91 | 92 | [[package]] 93 | name = "httpcore" 94 | version = "1.0.6" 95 | source = { registry = "https://pypi.org/simple" } 96 | dependencies = [ 97 | { name = "certifi" }, 98 | { name = "h11" }, 99 | ] 100 | sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 } 101 | wheels = [ 102 | { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 }, 103 | ] 104 | 105 | [[package]] 106 | name = "httpx" 107 | version = "0.27.2" 108 | source = { registry = "https://pypi.org/simple" } 109 | dependencies = [ 110 | { name = "anyio" }, 111 | { name = "certifi" }, 112 | { name = "httpcore" }, 113 | { name = "idna" }, 114 | { name = "sniffio" }, 115 | ] 116 | sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } 117 | wheels = [ 118 | { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, 119 | ] 120 | 121 | [[package]] 122 | name = "idna" 123 | version = "3.10" 124 | source = { registry = "https://pypi.org/simple" } 125 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 126 | wheels = [ 127 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 128 | ] 129 | 130 | [[package]] 131 | name = "pydantic" 132 | version = "2.9.2" 133 | source = { registry = "https://pypi.org/simple" } 134 | dependencies = [ 135 | { name = "annotated-types" }, 136 | { name = "pydantic-core" }, 137 | { name = "typing-extensions" }, 138 | ] 139 | sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } 140 | wheels = [ 141 | { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, 142 | ] 143 | 144 | [[package]] 145 | name = "pydantic-core" 146 | version = "2.23.4" 147 | source = { registry = "https://pypi.org/simple" } 148 | dependencies = [ 149 | { name = "typing-extensions" }, 150 | ] 151 | sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } 152 | wheels = [ 153 | { url = "https://files.pythonhosted.org/packages/5c/8b/d3ae387f66277bd8104096d6ec0a145f4baa2966ebb2cad746c0920c9526/pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", size = 1867835 }, 154 | { url = "https://files.pythonhosted.org/packages/46/76/f68272e4c3a7df8777798282c5e47d508274917f29992d84e1898f8908c7/pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", size = 1776689 }, 155 | { url = "https://files.pythonhosted.org/packages/cc/69/5f945b4416f42ea3f3bc9d2aaec66c76084a6ff4ff27555bf9415ab43189/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", size = 1800748 }, 156 | { url = "https://files.pythonhosted.org/packages/50/ab/891a7b0054bcc297fb02d44d05c50e68154e31788f2d9d41d0b72c89fdf7/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", size = 1806469 }, 157 | { url = "https://files.pythonhosted.org/packages/31/7c/6e3fa122075d78f277a8431c4c608f061881b76c2b7faca01d317ee39b5d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", size = 2002246 }, 158 | { url = "https://files.pythonhosted.org/packages/ad/6f/22d5692b7ab63fc4acbc74de6ff61d185804a83160adba5e6cc6068e1128/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", size = 2659404 }, 159 | { url = "https://files.pythonhosted.org/packages/11/ac/1e647dc1121c028b691028fa61a4e7477e6aeb5132628fde41dd34c1671f/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", size = 2053940 }, 160 | { url = "https://files.pythonhosted.org/packages/91/75/984740c17f12c3ce18b5a2fcc4bdceb785cce7df1511a4ce89bca17c7e2d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", size = 1921437 }, 161 | { url = "https://files.pythonhosted.org/packages/a0/74/13c5f606b64d93f0721e7768cd3e8b2102164866c207b8cd6f90bb15d24f/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", size = 1966129 }, 162 | { url = "https://files.pythonhosted.org/packages/18/03/9c4aa5919457c7b57a016c1ab513b1a926ed9b2bb7915bf8e506bf65c34b/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", size = 2110908 }, 163 | { url = "https://files.pythonhosted.org/packages/92/2c/053d33f029c5dc65e5cf44ff03ceeefb7cce908f8f3cca9265e7f9b540c8/pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", size = 1735278 }, 164 | { url = "https://files.pythonhosted.org/packages/de/81/7dfe464eca78d76d31dd661b04b5f2036ec72ea8848dd87ab7375e185c23/pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", size = 1917453 }, 165 | { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, 166 | { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, 167 | { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, 168 | { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, 169 | { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, 170 | { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, 171 | { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, 172 | { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, 173 | { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, 174 | { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, 175 | { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, 176 | { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, 177 | { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, 178 | { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, 179 | { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, 180 | { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, 181 | { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, 182 | { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, 183 | { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, 184 | { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, 185 | { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, 186 | { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, 187 | { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, 188 | { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, 189 | { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, 190 | { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, 191 | { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, 192 | { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, 193 | { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, 194 | { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, 195 | { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, 196 | { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, 197 | { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, 198 | { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, 199 | { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, 200 | { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, 201 | { url = "https://files.pythonhosted.org/packages/13/a9/5d582eb3204464284611f636b55c0a7410d748ff338756323cb1ce721b96/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", size = 1857135 }, 202 | { url = "https://files.pythonhosted.org/packages/2c/57/faf36290933fe16717f97829eabfb1868182ac495f99cf0eda9f59687c9d/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", size = 1740583 }, 203 | { url = "https://files.pythonhosted.org/packages/91/7c/d99e3513dc191c4fec363aef1bf4c8af9125d8fa53af7cb97e8babef4e40/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", size = 1793637 }, 204 | { url = "https://files.pythonhosted.org/packages/29/18/812222b6d18c2d13eebbb0f7cdc170a408d9ced65794fdb86147c77e1982/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", size = 1941963 }, 205 | { url = "https://files.pythonhosted.org/packages/0f/36/c1f3642ac3f05e6bb4aec3ffc399fa3f84895d259cf5f0ce3054b7735c29/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", size = 1915332 }, 206 | { url = "https://files.pythonhosted.org/packages/f7/ca/9c0854829311fb446020ebb540ee22509731abad886d2859c855dd29b904/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", size = 1957926 }, 207 | { url = "https://files.pythonhosted.org/packages/c0/1c/7836b67c42d0cd4441fcd9fafbf6a027ad4b79b6559f80cf11f89fd83648/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", size = 2100342 }, 208 | { url = "https://files.pythonhosted.org/packages/a9/f9/b6bcaf874f410564a78908739c80861a171788ef4d4f76f5009656672dfe/pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", size = 1920344 }, 209 | ] 210 | 211 | [[package]] 212 | name = "sniffio" 213 | version = "1.3.1" 214 | source = { registry = "https://pypi.org/simple" } 215 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 216 | wheels = [ 217 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 218 | ] 219 | 220 | [[package]] 221 | name = "typing-extensions" 222 | version = "4.12.2" 223 | source = { registry = "https://pypi.org/simple" } 224 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 225 | wheels = [ 226 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 227 | ] 228 | 229 | [[package]] 230 | name = "workspaces-app" 231 | version = "0.1.0" 232 | source = { virtual = "." } 233 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | --------------------------------------------------------------------------------