diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aef6dbd..760aa71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,8 @@ jobs: env: CI: "true" steps: + - name: Install uv + run: pip install uv - name: Checkout uses: actions/checkout@v4 with: diff --git a/.projen/deps.json b/.projen/deps.json index efa3840..2323239 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -4,6 +4,10 @@ "name": "@biomejs/biome", "type": "build" }, + { + "name": "@types/fs-extra", + "type": "build" + }, { "name": "@types/jest", "type": "build" @@ -17,6 +21,10 @@ "version": "^12", "type": "build" }, + { + "name": "fs-extra", + "type": "build" + }, { "name": "jest", "type": "build" diff --git a/.projen/tasks.json b/.projen/tasks.json index 8eec86f..aad0b76 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -230,7 +230,7 @@ "description": "Run tests", "steps": [ { - "exec": "jest --testTimeout=300000 --passWithNoTests --updateSnapshot", + "exec": "jest --testTimeout=400000 --passWithNoTests --updateSnapshot", "receiveArgs": true } ] @@ -269,13 +269,13 @@ }, "steps": [ { - "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" + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@biomejs/biome,@types/fs-extra,@types/jest,@types/node,fs-extra,jest,jsii-diff,jsii-pacmak,projen,ts-jest,ts-node,typescript" }, { "exec": "yarn install --check-files" }, { - "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" + "exec": "yarn upgrade @biomejs/biome @types/fs-extra @types/jest @types/node commit-and-tag-version fs-extra jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii projen ts-jest ts-node typescript aws-cdk-lib constructs" }, { "exec": "npx projen" diff --git a/.projenrc.ts b/.projenrc.ts index ec85fe2..5a4f46b 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1,4 +1,4 @@ -import { awscdk } from 'projen'; +import { JsonPatch, awscdk } from 'projen'; import { JobPermission } from 'projen/lib/github/workflows-model'; const project = new awscdk.AwsCdkConstructLibrary({ author: 'Eoin Shanaghy', @@ -18,13 +18,18 @@ const project = new awscdk.AwsCdkConstructLibrary({ // cdkDependencies: [], /* CDK dependencies of this module. */ // deps: [], /* Runtime dependencies of this module. */ // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ - devDeps: ['@biomejs/biome'] /* Build dependencies for this module. */, + devDeps: [ + '@biomejs/biome', + 'fs-extra', + '@types/fs-extra', + ] /* Build dependencies for this module. */, // packageName: undefined, /* The "name" in package.json. */ jestOptions: { - extraCliOptions: ['--testTimeout=300000'], + extraCliOptions: ['--testTimeout=400000'], }, eslint: false, }); + const biomeWorkflow = project.github?.addWorkflow('biome'); biomeWorkflow?.on({ pullRequest: { @@ -58,5 +63,15 @@ biomeWorkflow?.addJobs({ }, }); +const buildWorkflow = project.tryFindObjectFile('.github/workflows/build.yml'); +if (buildWorkflow) { + buildWorkflow.patch( + JsonPatch.add('/jobs/build/steps/0', { + name: 'Install uv', + run: 'pip install uv', + }), + ); +} + project.files; project.synth(); diff --git a/API.md b/API.md index 029babb..56d6d56 100644 --- a/API.md +++ b/API.md @@ -1155,6 +1155,7 @@ const bundlingOptions: BundlingOptions = { ... } | assetHashType | aws-cdk-lib.AssetHashType | Determines how asset hash is calculated. Assets will get rebuild and uploaded only if their hash has changed. | | buildArgs | {[ key: string ]: string} | Optional build arguments to pass to the default container. | | bundlingFileAccess | aws-cdk-lib.BundlingFileAccess | Which option to use to copy the source files to the docker container and output files back. | +| bundlingStrategy | BundlingStrategy | Strategy for bundling. | | commandHooks | ICommandHooks | Command hooks. | | image | aws-cdk-lib.DockerImage | Docker image to use for bundling. | | outputPathSuffix | string | Output path suffix: the suffix for the directory into which the bundled output is written. | @@ -1388,6 +1389,19 @@ Which option to use to copy the source files to the docker container and output --- +##### `bundlingStrategy`Optional + +```typescript +public readonly bundlingStrategy: BundlingStrategy; +``` + +- *Type:* BundlingStrategy +- *Default:* `BundlingStrategy.SOURCE` + +Strategy for bundling. + +--- + ##### `commandHooks`Optional ```typescript @@ -1460,6 +1474,7 @@ const bundlingProps: BundlingProps = { ... } | assetHashType | aws-cdk-lib.AssetHashType | Determines how asset hash is calculated. Assets will get rebuild and uploaded only if their hash has changed. | | buildArgs | {[ key: string ]: string} | Optional build arguments to pass to the default container. | | bundlingFileAccess | aws-cdk-lib.BundlingFileAccess | Which option to use to copy the source files to the docker container and output files back. | +| bundlingStrategy | BundlingStrategy | Strategy for bundling. | | commandHooks | ICommandHooks | Command hooks. | | image | aws-cdk-lib.DockerImage | Docker image to use for bundling. | | outputPathSuffix | string | Output path suffix: the suffix for the directory into which the bundled output is written. | @@ -1699,6 +1714,19 @@ Which option to use to copy the source files to the docker container and output --- +##### `bundlingStrategy`Optional + +```typescript +public readonly bundlingStrategy: BundlingStrategy; +``` + +- *Type:* BundlingStrategy +- *Default:* `BundlingStrategy.SOURCE` + +Strategy for bundling. + +--- + ##### `commandHooks`Optional ```typescript @@ -2985,3 +3013,25 @@ Commands are chained with `&&`. --- +## Enums + +### BundlingStrategy + +#### Members + +| **Name** | **Description** | +| --- | --- | +| SOURCE | *No description.* | +| GIT | *No description.* | + +--- + +##### `SOURCE` + +--- + + +##### `GIT` + +--- + diff --git a/package-lock.json b/package-lock.json index 81a318b..091ecd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.9.4", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.14", "@types/node": "^22.10.3", "aws-cdk-lib": "2.161.1", "commit-and-tag-version": "^12", "constructs": "10.3.0", + "fs-extra": "^11.2.0", "jest": "^29.7.0", "jest-junit": "^15", "jsii": "~5.5.0", @@ -1907,6 +1909,17 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1955,6 +1968,16 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -3097,6 +3120,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/codemaker/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -4225,9 +4263,9 @@ } }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "license": "MIT", "dependencies": { @@ -4236,7 +4274,7 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs.realpath": { @@ -6898,6 +6936,21 @@ "dev": true, "license": "MIT" }, + "node_modules/jsii-diff/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsii-diff/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6985,6 +7038,21 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/jsii-docgen/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsii-docgen/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -7177,6 +7245,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jsii-pacmak/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsii-pacmak/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7319,6 +7402,21 @@ "dev": true, "license": "MIT" }, + "node_modules/jsii-reflect/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsii-reflect/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/package.json b/package.json index 2d71de4..bcac4bd 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,13 @@ }, "devDependencies": { "@biomejs/biome": "^1.9.4", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.14", "@types/node": "^22.10.3", "aws-cdk-lib": "2.161.1", "commit-and-tag-version": "^12", "constructs": "10.3.0", + "fs-extra": "^11.2.0", "jest": "^29.7.0", "jest-junit": "^15", "jsii": "~5.5.0", diff --git a/src/bundling.ts b/src/bundling.ts index f6e72db..41aecf7 100644 --- a/src/bundling.ts +++ b/src/bundling.ts @@ -1,3 +1,5 @@ +import { execSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; import * as path from 'node:path'; import { AssetHashType, @@ -13,6 +15,7 @@ import { type Runtime, } from 'aws-cdk-lib/aws-lambda'; import type { BundlingOptions, ICommandHooks } from './types'; +import { BundlingStrategy } from './types'; export const HASHABLE_DEPENDENCIES_EXCLUDE = [ '*.pyc', @@ -86,13 +89,69 @@ export class Bundling { hashableAssetExclude = HASHABLE_DEPENDENCIES_EXCLUDE, ...bundlingOptions } = options; + switch (options.bundlingStrategy) { + case BundlingStrategy.GIT: + return Bundling.gitStrategy(bundlingOptions); + default: + return Bundling.sourceStrategy(bundlingOptions); + } + } + + /** + * Uses the AssetHashType.SOURCE strategy to calculate the asset hash. + * + * If there are multiple functions being created from a workspace they will all be rebuilt if the source changes. + * + * @param options + * @private + */ + private static sourceStrategy(options: BundlingProps): AssetCode { return Code.fromAsset(options.rootDir, { assetHashType: AssetHashType.SOURCE, - exclude: hashableAssetExclude, - bundling: new Bundling(bundlingOptions), + exclude: options.hashableAssetExclude, + bundling: new Bundling(options), }); } + private static gitStrategy(options: BundlingProps): AssetCode { + const workspacePackage = options.workspacePackage; + let assetHash: string; + + if (!workspacePackage) { + assetHash = Bundling.gitHash(options.rootDir); + } else { + const uvCommand = `cd ${options.rootDir} && uv export --package ${workspacePackage} --frozen --no-editable --no-dev --no-header`; + const dependencies = execSync(uvCommand).toString(); + // This includes the current workspacePackage + const workspaceDependencies = extractWorkspaceDependencies(dependencies); + const hash = createHash('sha256').update(dependencies); + for (const dependency of workspaceDependencies) { + hash.update(Bundling.gitHash(path.join(options.rootDir, dependency))); + } + assetHash = hash.digest('hex'); + } + + return Code.fromAsset(options.rootDir, { + assetHashType: AssetHashType.CUSTOM, + assetHash, + exclude: options.hashableAssetExclude, + bundling: new Bundling(options), + }); + } + + private static gitHash(path: string): string { + const gitCommands = [ + `cd ${path}`, + 'git log -1 --format=%H -- .', // find the hash of the last commit that changed the files in the directory + 'git diff -- .', // get the diff of the files in the directory and below + ]; + const gitCommand = gitCommands.join(' && '); + const status = execSync(gitCommand).toString().trim(); + const assetHash = createHash('sha256').update(status).digest('hex'); + + return assetHash; + } + public readonly image: DockerImage; public readonly entrypoint?: string[] | undefined; public readonly command: string[] | undefined; @@ -196,3 +255,24 @@ export class Bundling { return commands; } } + +function extractWorkspaceDependencies(content: string): string[] { + // Split content into lines and filter out empty lines + const lines = content.split('\n').filter((line) => line.trim()); + + // Regular expression to match package lines + // Matches lines containing package==version followed by hash(es) + const packageLineRegex = + /^[\w-]+==[0-9]+\.[0-9]+(\.[0-9]+)?([a-z0-9.]+)?\s*(\\|\s|$)/; + + // Filter out package lines and hash lines + return lines.filter((line) => { + // Skip hash lines + if (line.trim().startsWith('--hash=')) { + return false; + } + + // Keep lines that don't match package pattern + return !packageLineRegex.test(line); + }); +} diff --git a/src/types.ts b/src/types.ts index c5529b8..13eb2a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,6 +93,19 @@ export interface BundlingOptions extends DockerRunOptions { * @default - BundlingFileAccess.BIND_MOUNT */ readonly bundlingFileAccess?: BundlingFileAccess; + + /** + * Strategy for bundling + * + * @default - `BundlingStrategy.SOURCE` + * + */ + readonly bundlingStrategy?: BundlingStrategy; +} + +export enum BundlingStrategy { + SOURCE = 'source', + GIT = 'git', } /** diff --git a/test/function.test.ts b/test/function.test.ts index 1f4f22a..9970aad 100644 --- a/test/function.test.ts +++ b/test/function.test.ts @@ -7,7 +7,9 @@ import { App, Stack } from 'aws-cdk-lib'; import { Match, Template } from 'aws-cdk-lib/assertions'; import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; import * as cxapi from 'aws-cdk-lib/cx-api'; -import { PythonFunction } from '../src'; +import * as fsextra from 'fs-extra'; +import { BundlingStrategy, PythonFunction } from '../src'; + const execAsync = promisify(exec); const resourcesPath = path.resolve(__dirname, 'resources'); @@ -54,7 +56,7 @@ beforeEach(async () => { jest.resetModules(); process.env = { ...OLD_ENV }; process.env.CDK_OUTDIR = await fs.mkdtemp( - path.join(os.tmpdir(), 'uv-python-lambda-test-'), + path.join(os.tmpdir(), 'uv-python-lambda-function'), ); }); @@ -65,124 +67,829 @@ afterEach(async () => { process.env = OLD_ENV; }); -test('Create a function from basic_app', async () => { - const { app, stack } = await createStack(); +describe('When bundlingStrategy is set to BundlingStrategy.SOURCE', () => { + test('Create a function from basic_app', async () => { + const { app, stack } = await createStack(); - new PythonFunction(stack, 'basic_app', { - rootDir: path.join(resourcesPath, 'basic_app'), - index: 'handler.py', - handler: 'lambda_handler', - runtime: Runtime.PYTHON_3_12, - architecture: await getDockerHostArch(), - }); + new PythonFunction(stack, 'basic_app', { + rootDir: path.join(resourcesPath, 'basic_app'), + index: 'handler.py', + handler: 'lambda_handler', + runtime: Runtime.PYTHON_3_12, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.SOURCE, + }, + }); - const template = Template.fromStack(stack); + const template = Template.fromStack(stack); - template.hasResourceProperties('AWS::Lambda::Function', { - Handler: 'handler.lambda_handler', - Runtime: 'python3.12', - Code: { - S3Bucket: Match.anyValue(), - S3Key: Match.anyValue(), - }, - }); - const functions = Object.values( - template.findResources('AWS::Lambda::Function'), - ); - expect(functions).toHaveLength(1); - const contents = await getFunctionAssetContents(functions[0], app); - expect(contents).toContain('handler.py'); -}); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'handler.lambda_handler', + Runtime: 'python3.12', + Code: { + S3Bucket: Match.anyValue(), + S3Key: Match.anyValue(), + }, + }); + const functions = getFunctions(template); + expect(functions).toHaveLength(1); + const contents = await getAssetContent(functions[0], app); + expect(contents).toContain('handler.py'); + }, 40000); // need long timeout as working with file system -test('Create a function from basic_app with no .py index extension', async () => { - const { stack } = await createStack(); + test('Create a function from basic_app with no .py index extension', async () => { + const { stack } = await createStack(); - new PythonFunction(stack, 'basic_app', { - rootDir: path.join(resourcesPath, 'basic_app'), - index: 'handler', - handler: 'lambda_handler', - runtime: Runtime.PYTHON_3_12, - architecture: await getDockerHostArch(), + new PythonFunction(stack, 'basic_app', { + rootDir: path.join(resourcesPath, 'basic_app'), + index: 'handler', + handler: 'lambda_handler', + runtime: Runtime.PYTHON_3_12, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.SOURCE, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'handler.lambda_handler', + Runtime: 'python3.12', + Code: { + S3Bucket: Match.anyValue(), + S3Key: Match.anyValue(), + }, + }); }); - const template = Template.fromStack(stack); + test('Create a function from basic_app when skip is true', async () => { + const { stack } = await createStack(); + + const bundlingSpy = jest + .spyOn(stack, 'bundlingRequired', 'get') + .mockReturnValue(false); + const architecture = await getDockerHostArch(); + + // To see this fail, comment out the `if (skip) { return; } code in the PythonFunction constructor + expect(() => { + new PythonFunction(stack, 'basic_app', { + rootDir: path.join(resourcesPath, 'basic_app'), + index: 'handler', + handler: 'lambda_handler', + runtime: Runtime.PYTHON_3_12, + architecture, + bundling: { + bundlingStrategy: BundlingStrategy.SOURCE, + }, + }); + }).not.toThrow(); - template.hasResourceProperties('AWS::Lambda::Function', { - Handler: 'handler.lambda_handler', - Runtime: 'python3.12', - Code: { - S3Bucket: Match.anyValue(), - S3Key: Match.anyValue(), - }, + bundlingSpy.mockRestore(); }); + + test('Create a function with workspaces_app', async () => { + const { app, stack } = await createStack('wstest'); + + new PythonFunction(stack, 'workspaces_app', { + rootDir: path.join(resourcesPath, 'workspaces_app'), + workspacePackage: 'app', + index: 'app_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.SOURCE, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'app_handler.handle_event', + Runtime: 'python3.10', + Code: { + S3Bucket: Match.anyValue(), + S3Key: Match.anyValue(), + }, + }); + + const functions = getFunctions(template); + expect(functions).toHaveLength(1); + const contents = await getAssetContent(functions[0], app); + for (const entry of [ + 'app', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(contents).toContain(entry); + } + }, 40000); // need long timeout as working with file system + + test('Create multiple functions with workspaces_app', async () => { + const { app, stack } = await createStack('wstest'); + + new PythonFunction(stack, 'workspaces_app', { + rootDir: path.join(resourcesPath, 'workspaces_app'), + workspacePackage: 'app', + index: 'app_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.SOURCE, + }, + }); + + new PythonFunction(stack, 'workspaces_backend', { + rootDir: path.join(resourcesPath, 'workspaces_app'), + workspacePackage: 'backend', + index: 'backend_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.SOURCE, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'app_handler.handle_event', + }); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'backend_handler.handle_event', + }); + + const functions = getFunctions(template); + expect(functions).toHaveLength(2); + const appContents = await getAssetContent(functions[0], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(appContents).toContain(entry); + } + expect(appContents).not.toContain('backend_handler.py'); + expect(appContents).not.toContain('pathlib'); + const backendContents = await getAssetContent(functions[1], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'backend_handler.py', + ]) { + expect(backendContents).toContain(entry); + } + expect(backendContents).not.toContain('app_handler.py'); + expect(backendContents).not.toContain('flask'); + }, 30000); }); -test('Create a function from basic_app when skip is true', async () => { - const { stack } = await createStack(); +describe('When bundlingStrategy is set to BundlingStrategy.GIT', () => { + jest.setTimeout(30000); // we are doing integration tests with the file system so give tests more time - const bundlingSpy = jest - .spyOn(stack, 'bundlingRequired', 'get') - .mockReturnValue(false); - const architecture = await getDockerHostArch(); + test('Create a function from basic_app', async () => { + const { app, stack } = await createStack(); - // To see this fail, comment out the `if (skip) { return; } code in the PythonFunction constructor - expect(() => { new PythonFunction(stack, 'basic_app', { rootDir: path.join(resourcesPath, 'basic_app'), - index: 'handler', + index: 'handler.py', handler: 'lambda_handler', runtime: Runtime.PYTHON_3_12, - architecture, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, }); - }).not.toThrow(); - bundlingSpy.mockRestore(); -}); + const template = Template.fromStack(stack); -test('Create a function with workspaces_app', async () => { - const { app, stack } = await createStack('wstest'); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'handler.lambda_handler', + Runtime: 'python3.12', + Code: { + S3Bucket: Match.anyValue(), + S3Key: Match.anyValue(), + }, + }); + const functions = getFunctions(template); + expect(functions).toHaveLength(1); + const contents = await getAssetContent(functions[0], app); + expect(contents).toContain('handler.py'); + }); + + test('Create a function from basic_app when skip is true', async () => { + const { stack } = await createStack(); + + const bundlingSpy = jest + .spyOn(stack, 'bundlingRequired', 'get') + .mockReturnValue(false); + const architecture = await getDockerHostArch(); - new PythonFunction(stack, 'workspaces_app', { - rootDir: path.join(resourcesPath, 'workspaces_app'), - workspacePackage: 'app', - index: 'app_handler.py', - handler: 'handle_event', - runtime: Runtime.PYTHON_3_10, - architecture: await getDockerHostArch(), + // To see this fail, comment out the `if (skip) { return; } code in the PythonFunction constructor + expect(() => { + new PythonFunction(stack, 'basic_app', { + rootDir: path.join(resourcesPath, 'basic_app'), + index: 'handler', + handler: 'lambda_handler', + runtime: Runtime.PYTHON_3_12, + architecture, + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, + }); + }).not.toThrow(); + + bundlingSpy.mockRestore(); }); - const template = Template.fromStack(stack); + test('Create a function from basic_app only gets rebuild when source changes', async () => { + const createBasicAppStack = async (workspacePath: string) => { + const { app, stack } = await createStack(); + + new PythonFunction(stack, 'basic_app', { + rootDir: path.join(workspacePath, 'basic_app'), + index: 'handler.py', + handler: 'lambda_handler', + runtime: Runtime.PYTHON_3_12, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, + }); + + return { app, stack }; + }; - template.hasResourceProperties('AWS::Lambda::Function', { - Handler: 'app_handler.handle_event', - Runtime: 'python3.10', - Code: { - S3Bucket: Match.anyValue(), - S3Key: Match.anyValue(), - }, + const workspacePath = await copyWorkspaceToTemp('basic_app', { git: true }); + + let { app, stack } = await createBasicAppStack(workspacePath); + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + const basicAppAssetPath = getAssetPath(run1Functions[0], app); + expect(run1Functions).toHaveLength(1); + const contents = await getAssetContent(run1Functions[0], app); + expect(contents).toContain('handler.py'); + // change the contents of the folder so we can verify what is rebuilt + await setAssetContents(run1Functions[0], app, ['basic_app.text']); + + // Need to create the stack again as synthesize is only called once otherwise + ({ app, stack } = await createBasicAppStack(workspacePath)); + template = Template.fromStack(stack); + const run2Functions = getFunctions(template); + // validate that same folder is used because the hash is the same + expect(getAssetPath(run2Functions[0], app)).toEqual(basicAppAssetPath); + expect(await getAssetContent(run2Functions[0], app)).toEqual([ + 'basic_app.text', + ]); + + // Now modify the source folder and check that the function is rebuilt + await replaceStringInFile( + path.join(workspacePath, 'basic_app', 'handler.py'), + 'Hello', + 'Hi', + ); + + ({ app, stack } = await createBasicAppStack(workspacePath)); + template = Template.fromStack(stack); + const run3Functions = getFunctions(template); + // validate that different folder is used because the hash is different + expect(getAssetPath(run3Functions[0], app)).not.toEqual(basicAppAssetPath); + expect(await getAssetContent(run3Functions[0], app)).toContain( + 'handler.py', + ); }); - const functions = Object.values( - template.findResources('AWS::Lambda::Function'), - ); - expect(functions).toHaveLength(1); - const contents = await getFunctionAssetContents(functions[0], app); - for (const entry of [ - 'app', - 'common', - 'pydantic', - 'httpx', - '_common.pth', - 'app_handler.py', - ]) { - expect(contents).toContain(entry); + test('Create a function with workspaces_app', async () => { + const { app, stack } = await createStack('wstest'); + + new PythonFunction(stack, 'workspaces_app', { + rootDir: path.join(resourcesPath, 'workspaces_app'), + workspacePackage: 'app', + index: 'app_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'app_handler.handle_event', + Runtime: 'python3.10', + Code: { + S3Bucket: Match.anyValue(), + S3Key: Match.anyValue(), + }, + }); + + const functions = getFunctions(template); + expect(functions).toHaveLength(1); + const contents = await getAssetContent(functions[0], app); + for (const entry of [ + 'app', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(contents).toContain(entry); + } + }); + + const createWorkspaceAppStack = async (workspacePath: string) => { + const { app, stack } = await createStack(); + + new PythonFunction(stack, 'workspaces_app', { + rootDir: path.join(workspacePath, 'workspaces_app'), + workspacePackage: 'app', + index: 'app_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, + }); + + return { app, stack }; + }; + + test('Create a function with workspaces_app gets rebuilt if app changes', async () => { + const workspacePath = await copyWorkspaceToTemp('workspaces_app', { + git: true, + }); + + let { app, stack } = await createWorkspaceAppStack(workspacePath); + + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + expect(run1Functions).toHaveLength(1); + const appAssetPath = getAssetPath(run1Functions[0], app); + const contents = await getAssetContent(run1Functions[0], app); + for (const entry of [ + 'app', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(contents).toContain(entry); + } + + // change the contents of the folder so we can verify what is rebuilt + await setAssetContents(run1Functions[0], app, ['app.text']); + + // Need to create the stack again as synthesize is only called once otherwise + ({ app, stack } = await createWorkspaceAppStack(workspacePath)); + template = Template.fromStack(stack); + const run2Functions = getFunctions(template); + // validate that same folder is used because the hash is the same + expect(getAssetPath(run2Functions[0], app)).toEqual(appAssetPath); + expect(await getAssetContent(run2Functions[0], app)).toEqual(['app.text']); + + // Now modify the source folder and check that the function is rebuilt + await replaceStringInFile( + path.join(workspacePath, 'workspaces_app', 'app', 'app_handler.py'), + 'Corolla', + 'Auris', + ); + + ({ app, stack } = await createWorkspaceAppStack(workspacePath)); + template = Template.fromStack(stack); + const run3Functions = getFunctions(template); + const run3AssetPath = getAssetPath(run3Functions[0], app); + // validate that different folder is used because the hash is different + expect(run3AssetPath).not.toEqual(appAssetPath); + expect(await getAssetContent(run3Functions[0], app)).toContain( + 'app_handler.py', + ); + }); + + test('Create a function with workspaces_app gets rebuilt if dependency changes', async () => { + const workspacePath = await copyWorkspaceToTemp('workspaces_app', { + git: true, + }); + + let { app, stack } = await createWorkspaceAppStack(workspacePath); + + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + expect(run1Functions).toHaveLength(1); + const appAssetPath = getAssetPath(run1Functions[0], app); + const contents = await getAssetContent(run1Functions[0], app); + for (const entry of [ + 'app', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(contents).toContain(entry); + } + + // change the contents of the folder so we can verify what is rebuilt + await setAssetContents(run1Functions[0], app, ['app.text']); + + // Now modify the dependency source folder and check that the function is rebuilt + await replaceStringInFile( + path.join(workspacePath, 'workspaces_app', 'common', 'README.md'), + 'Common', + 'Shared', + ); + + ({ app, stack } = await createWorkspaceAppStack(workspacePath)); + template = Template.fromStack(stack); + const run3Functions = getFunctions(template); + const run3AssetPath = getAssetPath(run3Functions[0], app); + // validate that different folder is used because the hash is different + expect(run3AssetPath).not.toEqual(appAssetPath); + expect(await getAssetContent(run3Functions[0], app)).toContain( + 'app_handler.py', + ); + }); + + test('Create a function with workspaces_app does NOT get rebuilt if unrelated code changes', async () => { + const workspacePath = await copyWorkspaceToTemp('workspaces_app', { + git: true, + }); + + let { app, stack } = await createWorkspaceAppStack(workspacePath); + + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + expect(run1Functions).toHaveLength(1); + const appAssetPath = getAssetPath(run1Functions[0], app); + const contents = await getAssetContent(run1Functions[0], app); + for (const entry of [ + 'app', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(contents).toContain(entry); + } + + // change the contents of the folder so we can verify what is rebuilt + await setAssetContents(run1Functions[0], app, ['app.text']); + + // Now modify the unrelated app source and check that the function is not rebuilt + await replaceStringInFile( + path.join( + workspacePath, + 'workspaces_app', + 'backend', + 'backend_handler.py', + ), + 'Skyline', + 'Juke', + ); + + ({ app, stack } = await createWorkspaceAppStack(workspacePath)); + template = Template.fromStack(stack); + const run3Functions = getFunctions(template); + const run3AssetPath = getAssetPath(run3Functions[0], app); + // validate that same folder is used because the hash is the same + expect(run3AssetPath).toEqual(appAssetPath); + expect(await getAssetContent(run3Functions[0], app)).toContain('app.text'); + }); + + async function createMultipleFunctionStack( + workspacePath?: string, + ): Promise<{ app: App; stack: Stack }> { + const { app, stack } = await createStack('wstest'); + const rootPath = workspacePath ?? resourcesPath; + + new PythonFunction(stack, 'workspaces_app', { + rootDir: path.join(rootPath, 'workspaces_app'), + workspacePackage: 'app', + index: 'app_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, + }); + + new PythonFunction(stack, 'workspaces_backend', { + rootDir: path.join(rootPath, 'workspaces_app'), + workspacePackage: 'backend', + index: 'backend_handler.py', + handler: 'handle_event', + runtime: Runtime.PYTHON_3_10, + architecture: await getDockerHostArch(), + bundling: { + bundlingStrategy: BundlingStrategy.GIT, + }, + }); + + return { app, stack }; } + + test('Create multiple functions with workspaces_app', async () => { + const { app, stack } = await createMultipleFunctionStack(); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'app_handler.handle_event', + }); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'backend_handler.handle_event', + }); + + const functions = getFunctions(template); + expect(functions).toHaveLength(2); + const appContents = await getAssetContent(functions[0], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(appContents).toContain(entry); + } + expect(appContents).not.toContain('backend_handler.py'); + expect(appContents).not.toContain('pathlib'); + const backendContents = await getAssetContent(functions[1], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'backend_handler.py', + ]) { + expect(backendContents).toContain(entry); + } + expect(backendContents).not.toContain('app_handler.py'); + expect(backendContents).not.toContain('flask'); + }); + + test("Doesn't rebuild multiple functions if they haven't changed", async () => { + let { app, stack } = await createMultipleFunctionStack(); + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + const appAssetPath = getAssetPath(run1Functions[0], app); + const backendAssetPath = getAssetPath(run1Functions[1], app); + // change the contents of the folder so we can verify they are not rebuilt + await setAssetContents(run1Functions[0], app, ['app.text']); + await setAssetContents(run1Functions[1], app, ['backend.text']); + + // Need to create the stack again as synthesize is only called once otherwise + ({ app, stack } = await createMultipleFunctionStack()); + + template = Template.fromStack(stack); + const run2Functions = getFunctions(template); + // validate that the same folder is being used for the functions + expect(getAssetPath(run2Functions[0], app)).toEqual(appAssetPath); + expect(getAssetPath(run2Functions[1], app)).toEqual(backendAssetPath); + + // validate they contain the content we put in above, i.e. haven't been overwritten + expect(await getAssetContent(run2Functions[0], app)).toEqual(['app.text']); + expect(await getAssetContent(run2Functions[1], app)).toEqual([ + 'backend.text', + ]); + }); + + test('Rebuild single function when its content changes', async () => { + // copy the workspace so we can edit it without affecting the original + const workspacePath = await copyWorkspaceToTemp('workspaces_app', { + git: true, + }); + let { app, stack } = await createMultipleFunctionStack(workspacePath); + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + const appAssetPath = getAssetPath(run1Functions[0], app); + const backendAssetPath = getAssetPath(run1Functions[1], app); + // change the contents of the folder so we can verify what is rebuilt + await setAssetContents(run1Functions[0], app, ['app.text']); + await setAssetContents(run1Functions[1], app, ['backend.text']); + + // Now modify the source folder and check that the function is rebuilt + await replaceStringInFile( + path.join(workspacePath, 'workspaces_app', 'app', 'app_handler.py'), + 'Corolla', + 'Auris', + ); + // Need to create the stack again as synthesize is only called once otherwise + ({ app, stack } = await createMultipleFunctionStack(workspacePath)); + + template = Template.fromStack(stack); + const run2Functions = getFunctions(template); + // validate that different folder for updated function but same one for unchanged + expect(getAssetPath(run2Functions[0], app)).not.toEqual(appAssetPath); + expect(getAssetPath(run2Functions[1], app)).toEqual(backendAssetPath); + + // validate app has been rebuild but backend hasn't been touched + const appContents = await getAssetContent(run2Functions[0], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(appContents).toContain(entry); + } + expect(await getAssetContent(run2Functions[1], app)).toEqual([ + 'backend.text', + ]); + }); + + test('Rebuild any function when its dependency version changes', async () => { + // copy the workspace so we can edit it without affecting the original + const workspacePath = await copyWorkspaceToTemp('workspaces_app', { + git: true, + }); + let { app, stack } = await createMultipleFunctionStack(workspacePath); + let template = Template.fromStack(stack); + + const run1Functions = getFunctions(template); + const appAssetPath = getAssetPath(run1Functions[0], app); + const backendAssetPath = getAssetPath(run1Functions[1], app); + // change the contents of the folder so we can verify what is rebuilt + await setAssetContents(run1Functions[0], app, ['app.text']); + await setAssetContents(run1Functions[1], app, ['backend.text']); + + // Now modify the dependency source folder and check that both functions are rebuilt + await replaceStringInFile( + path.join(workspacePath, 'workspaces_app', 'common', 'README.md'), + 'Common', + 'Shared', + ); + // Need to create the stack again as synthesize is only called once otherwise + ({ app, stack } = await createMultipleFunctionStack(workspacePath)); + + template = Template.fromStack(stack); + const run2Functions = getFunctions(template); + // validate that different folder for both updated functions + expect(getAssetPath(run2Functions[0], app)).not.toEqual(appAssetPath); + expect(getAssetPath(run2Functions[1], app)).not.toEqual(backendAssetPath); + + // validate app and backend have both been rebuilt + const appContents = await getAssetContent(run2Functions[0], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'app_handler.py', + ]) { + expect(appContents).toContain(entry); + } + const backendContents = await getAssetContent(run2Functions[1], app); + for (const entry of [ + 'app', + 'backend', + 'common', + 'pydantic', + 'httpx', + '_common.pth', + 'backend_handler.py', + ]) { + expect(backendContents).toContain(entry); + } + }); }); -// biome-ignore lint/suspicious/noExplicitAny: -async function getFunctionAssetContents(functionResource: any, app: App) { +/** + * Copy the workspace to a temporary directory and return the path to the temporary directory. + * The workspace is copied to ensure that the original workspace is not modified when we change it during tests. + * + * @param workspace + * @param git - if true, a git repository is initialized in the copied workspace + */ +async function copyWorkspaceToTemp( + workspace: string, + { git = false }: { git?: boolean } = { git: false }, +): Promise { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'uv-python-lambda-workspace'), + ); + await fsextra.copy( + path.join(resourcesPath, workspace), + path.join(tempDir, workspace), + ); + + if (git) { + const gitPath = path.join(tempDir, workspace); + await execAsync('git init', { cwd: gitPath }); + await execAsync('git add .', { cwd: gitPath }); + await execAsync('git config user.email "test@example.com"', { + cwd: gitPath, + }); + await execAsync('git config user.name "Testy McTestface"', { + cwd: gitPath, + }); + + await execAsync('git commit -m "commit for test purposes"', { + cwd: gitPath, + }); + } + return tempDir; +} + +/** + * Replace all occurrences of a string in a file. Used to make modifications to workspace copies for testing purposes. + * + * @param filePath + * @param searchString + * @param replaceString + */ +async function replaceStringInFile( + filePath: string, + searchString: string, + replaceString: string, +): Promise { + try { + // Read the file content + const content = await fs.readFile(filePath, 'utf-8'); + + // Perform the replacement + const updatedContent = content.replace( + new RegExp(searchString, 'g'), + replaceString, + ); + + // Write the modified content back to the file + await fs.writeFile(filePath, updatedContent, 'utf-8'); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to replace string in file: ${error.message}`); + } + throw error; + } +} + +// biome-ignore lint/suspicious/noExplicitAny: any is what is returned from the CDK template.findResources method +function getFunctions(template: Template): any[] { + return Object.values(template.findResources('AWS::Lambda::Function')); +} + +// biome-ignore lint/suspicious/noExplicitAny: any is what is returned from the CDK template.findResources method +function getAssetPath(functionResource: any, app: App): string { const [assetHash] = functionResource.Properties.Code.S3Key.split('.'); - const assetPath = path.join(app.outdir, `asset.${assetHash}`); - const contents = await fs.readdir(assetPath); - return contents; + return path.join(app.outdir, `asset.${assetHash}`); +} + +// biome-ignore lint/suspicious/noExplicitAny: any is what is returned from the CDK template.findResources method +async function getAssetContent(functionResource: any, app: App) { + return await fs.readdir(getAssetPath(functionResource, app)); +} + +async function setAssetContents( + // biome-ignore lint/suspicious/noExplicitAny: any is what is returned from the CDK template.findResources method + functionResource: any, + app: App, + contents: string[], +) { + const assetPath = getAssetPath(functionResource, app); + + // remove all existing contents and add the new ones as empty files + const existingContents = await fs.readdir(assetPath); + for (const entry of existingContents) { + await fs.rm(path.join(assetPath, entry), { force: true, recursive: true }); + } + for (const entry of contents) { + await fs.writeFile(path.join(assetPath, entry), ''); + } } diff --git a/test/resources/workspaces_app/app/app_handler.py b/test/resources/workspaces_app/app/app_handler.py index 9f5af87..d9bdaae 100644 --- a/test/resources/workspaces_app/app/app_handler.py +++ b/test/resources/workspaces_app/app/app_handler.py @@ -1,10 +1,5 @@ -from pydantic import BaseModel - -class Car(BaseModel): - brand: str - model: str - year: int +from common.car import Car def handle_event(event, context): car = Car(brand="Toyota", model="Corolla", year=2020) - return car.model_dump() \ No newline at end of file + return car.model_dump() diff --git a/test/resources/workspaces_app/app/pyproject.toml b/test/resources/workspaces_app/app/pyproject.toml index 75a3019..47d74cb 100644 --- a/test/resources/workspaces_app/app/pyproject.toml +++ b/test/resources/workspaces_app/app/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "common", - "pydantic>=2.9.2", + "flask>=3.1.0", ] [tool.uv.sources] diff --git a/test/resources/workspaces_app/backend/.python-version b/test/resources/workspaces_app/backend/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/test/resources/workspaces_app/backend/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/test/resources/workspaces_app/backend/README.md b/test/resources/workspaces_app/backend/README.md new file mode 100644 index 0000000..e69de29 diff --git a/test/resources/workspaces_app/backend/backend_handler.py b/test/resources/workspaces_app/backend/backend_handler.py new file mode 100644 index 0000000..6c3e595 --- /dev/null +++ b/test/resources/workspaces_app/backend/backend_handler.py @@ -0,0 +1,5 @@ +from common.car import Car + +def handle_event(event, context): + car = Car(brand="Nissan", model="Skyline", year=2022) + return car.model_dump() diff --git a/test/resources/workspaces_app/backend/pyproject.toml b/test/resources/workspaces_app/backend/pyproject.toml new file mode 100644 index 0000000..d84c3c1 --- /dev/null +++ b/test/resources/workspaces_app/backend/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "backend" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "common", + "pathlib>=1.0.1", +] + +[tool.uv.sources] +common = { workspace = true } diff --git a/test/resources/workspaces_app/common/README.md b/test/resources/workspaces_app/common/README.md index e69de29..418e2f2 100644 --- a/test/resources/workspaces_app/common/README.md +++ b/test/resources/workspaces_app/common/README.md @@ -0,0 +1 @@ +Common code for all workspaces apps diff --git a/test/resources/workspaces_app/common/pyproject.toml b/test/resources/workspaces_app/common/pyproject.toml index 25a22a3..7f1d0c1 100644 --- a/test/resources/workspaces_app/common/pyproject.toml +++ b/test/resources/workspaces_app/common/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", + "pydantic>=2.9.2", ] [project.scripts] diff --git a/test/resources/workspaces_app/common/src/common/car.py b/test/resources/workspaces_app/common/src/common/car.py new file mode 100644 index 0000000..f0f37a2 --- /dev/null +++ b/test/resources/workspaces_app/common/src/common/car.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +class Car(BaseModel): + brand: str + model: str + year: int diff --git a/test/resources/workspaces_app/pyproject.toml b/test/resources/workspaces_app/pyproject.toml index 4af2b65..7c347fa 100644 --- a/test/resources/workspaces_app/pyproject.toml +++ b/test/resources/workspaces_app/pyproject.toml @@ -7,15 +7,16 @@ requires-python = ">=3.10" dependencies = [] [tool.uv.workspace] -members = ["common", "app"] +members = ["common", "app", "backend"] [tool.uv.sources] common = { workspace = true } app = { workspace = true } +backend = { workspace = true } [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["app", "common"] +packages = ["app", "backend", "common"] diff --git a/test/resources/workspaces_app/uv.lock b/test/resources/workspaces_app/uv.lock index deb98d5..c34f8ff 100644 --- a/test/resources/workspaces_app/uv.lock +++ b/test/resources/workspaces_app/uv.lock @@ -8,6 +8,7 @@ resolution-markers = [ [manifest] members = [ "app", + "backend", "common", "workspaces-app", ] @@ -42,13 +43,37 @@ version = "0.1.0" source = { editable = "app" } dependencies = [ { name = "common" }, - { name = "pydantic" }, + { name = "flask" }, ] [package.metadata] requires-dist = [ { name = "common", editable = "common" }, - { name = "pydantic", specifier = ">=2.9.2" }, + { name = "flask", specifier = ">=3.1.0" }, +] + +[[package]] +name = "backend" +version = "0.1.0" +source = { editable = "backend" } +dependencies = [ + { name = "common" }, + { name = "pathlib" }, +] + +[package.metadata] +requires-dist = [ + { name = "common", editable = "common" }, + { name = "pathlib", specifier = ">=1.0.1" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, ] [[package]] @@ -60,16 +85,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + [[package]] name = "common" version = "0.1.0" source = { editable = "common" } dependencies = [ { name = "httpx" }, + { name = "pydantic" }, ] [package.metadata] -requires-dist = [{ name = "httpx", specifier = ">=0.27.2" }] +requires-dist = [ + { name = "httpx", specifier = ">=0.27.2" }, + { name = "pydantic", specifier = ">=2.9.2" }, +] [[package]] name = "exceptiongroup" @@ -80,6 +130,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] +[[package]] +name = "flask" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -127,6 +193,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "pathlib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363 }, +] + [[package]] name = "pydantic" version = "2.9.2" @@ -226,6 +380,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +] + [[package]] name = "workspaces-app" version = "0.1.0" diff --git a/yarn.lock b/yarn.lock index ad31660..ec49906 100644 --- a/yarn.lock +++ b/yarn.lock @@ -335,7 +335,7 @@ "@biomejs/cli-darwin-arm64@1.9.4": version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz#dfa376d23a54a2d8f17133c92f23c1bf2e62509f" + resolved "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz" integrity sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw== "@biomejs/cli-darwin-x64@1.9.4": @@ -355,12 +355,12 @@ "@biomejs/cli-linux-x64-musl@1.9.4": version "1.9.4" - resolved "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz#f36982b966bd671a36671e1de4417963d7db15fb" integrity sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg== "@biomejs/cli-linux-x64@1.9.4": version "1.9.4" - resolved "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz#a0a7f56680c76b8034ddc149dbf398bdd3a462e8" integrity sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg== "@biomejs/cli-win32-arm64@1.9.4": @@ -783,6 +783,14 @@ dependencies: "@babel/types" "^7.20.7" +"@types/fs-extra@^11.0.4": + version "11.0.4" + resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz" + integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + "@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" @@ -817,6 +825,13 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jsonfile@*": + version "6.1.4" + resolved "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + dependencies: + "@types/node" "*" + "@types/minimist@^1.2.0": version "1.2.5" resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz" @@ -1872,7 +1887,7 @@ fs.realpath@^1.0.0: fsevents@^2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: