From 374c3bcf32e2bb9863671d657750c30a93bce381 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 13:32:14 -0700 Subject: [PATCH 01/36] chore: add CDK for basic CI resources --- cdk/.gitignore | 8 + cdk/.npmignore | 6 + cdk/README.md | 14 + cdk/cdk.json | 57 + cdk/jest.config.js | 8 + cdk/lib/cdk-stack.ts | 142 ++ cdk/package-lock.json | 4382 +++++++++++++++++++++++++++++++++++++++++ cdk/package.json | 27 + cdk/test/cdk.test.ts | 17 + cdk/tsconfig.json | 31 + 10 files changed, 4692 insertions(+) create mode 100644 cdk/.gitignore create mode 100644 cdk/.npmignore create mode 100644 cdk/README.md create mode 100644 cdk/cdk.json create mode 100644 cdk/jest.config.js create mode 100644 cdk/lib/cdk-stack.ts create mode 100644 cdk/package-lock.json create mode 100644 cdk/package.json create mode 100644 cdk/test/cdk.test.ts create mode 100644 cdk/tsconfig.json diff --git a/cdk/.gitignore b/cdk/.gitignore new file mode 100644 index 00000000..f60797b6 --- /dev/null +++ b/cdk/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cdk/.npmignore b/cdk/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cdk/README.md b/cdk/README.md new file mode 100644 index 00000000..320efc02 --- /dev/null +++ b/cdk/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `cdk deploy` deploy this stack to your default AWS account/region +* `cdk diff` compare deployed stack with current state +* `cdk synth` emits the synthesized CloudFormation template diff --git a/cdk/cdk.json b/cdk/cdk.json new file mode 100644 index 00000000..a7260af7 --- /dev/null +++ b/cdk/cdk.json @@ -0,0 +1,57 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true + } +} diff --git a/cdk/jest.config.js b/cdk/jest.config.js new file mode 100644 index 00000000..08263b89 --- /dev/null +++ b/cdk/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts new file mode 100644 index 00000000..88f31473 --- /dev/null +++ b/cdk/lib/cdk-stack.ts @@ -0,0 +1,142 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { + Alias, + Key +} from "aws-cdk-lib/aws-kms"; +import { + Effect, + Role, + PolicyDocument, + PolicyStatement, + FederatedPrincipal, + ManagedPolicy, +} from "aws-cdk-lib/aws-iam"; +import { + BlockPublicAccess, + BlockPublicAccessOptions, + Bucket, +} from 'aws-cdk-lib/aws-s3'; + +export class S3ECPythonGithub extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // KMS Key - default policy is fine, + // we use IAM to manage key permissions + const S3ECGithubKMSKey = new Key( + this, + "S3ECGithubKMSKey", + { + enableKeyRotation: true, + description: "KMS Key for GitHub Action Workflow", + } + ) + + // KMS alias + const S3ECGithubKMSKeyAlias = new Alias( + this, + "S3ECGithubKMSKeyAlias", + { + aliasName: "alias/S3EC-Python-Github-KMS-Key", + targetKey: S3ECGithubKMSKey + } + ) + + // S3 bucket + const AccessConfiguration: BlockPublicAccessOptions = { + blockPublicAcls: false, + blockPublicPolicy: false, + ignorePublicAcls: false, + restrictPublicBuckets: false + } + const S3ECGithubTestS3Bucket = new Bucket( + this, + "S3ECGithubTestS3Bucket", + { + bucketName: "s3ec-python-github-test-bucket", + blockPublicAccess: new BlockPublicAccess(AccessConfiguration) + } + ) + + // S3 bucket policy + const S3ECGithubS3BucketPolicy = new ManagedPolicy( + this, + "S3EC-Python-Github-S3-Bucket-Policy", + { + document: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject", + ], + resources: [ + S3ECGithubTestS3Bucket.bucketArn + "/*", // object-level permissions need this extra path + ], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + "s3:ListBucket", + ], + resources: [ + S3ECGithubTestS3Bucket.bucketArn + ], + }), + ] + }), + } + ); + + // KMS key policy + const S3ECGithubKMSKeyPolicy = new ManagedPolicy( + this, + "S3EC-Python-Github-KMS-Key-Policy", + { + document: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair" + ], + resources: [ + S3ECGithubKMSKey.keyArn, + ] + }) + ] + }), + } + ) + + // IAM role + const GithubActionsPrincipal = new FederatedPrincipal( + "arn:aws:iam::" + this.account + ":oidc-provider/token.actions.githubusercontent.com", + { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:aws/amazon-s3-encryption-client-python:*" + } + }, + "sts:AssumeRoleWithWebIdentity" + ) + const S3ECGithubTestRole = new Role( + this, + "s3-github-test-role", + { + assumedBy: GithubActionsPrincipal, + roleName: "S3EC-Python-Github-test-role", + description: " Grant GitHub S3 put and get and KMS encrypt, decrypt, and generate access for testing", + path: "/", + managedPolicies: [S3ECGithubS3BucketPolicy, S3ECGithubKMSKeyPolicy] + } + ); + } +} diff --git a/cdk/package-lock.json b/cdk/package-lock.json new file mode 100644 index 00000000..4f44562c --- /dev/null +++ b/cdk/package-lock.json @@ -0,0 +1,4382 @@ +{ + "name": "cdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cdk", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.92.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "cdk": "bin/cdk.js" + }, + "devDependencies": { + "@types/jest": "^29.5.3", + "@types/node": "20.4.10", + "aws-cdk": "2.92.0", + "jest": "^29.6.2", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.1.6" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.247", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.247.tgz", + "integrity": "sha512-PGFzztdu5YozUgoUd8gq5qi1FR3EYMjNrl5JFrAlYh2w1PcTfExEwqDzZy9z6uzogEJKwQJDgyhWe+OcZzQqFg==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-kubectl-v20": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.4.tgz", + "integrity": "sha512-Ps2MkmjYgMyflagqQ4dgTElc7Vwpqj8spw8dQVFiSeaaMPsuDSNsPax3/HjuDuwqsmLdaCZc6umlxYLpL0kYDA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.4.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.10.tgz", + "integrity": "sha512-vwzFiiy8Rn6E0MtA13/Cxxgpan/N6UeNYR9oUu6kuJWxu6zCk98trcDp8CBhbtaeuq9SykCmXkFr2lWLoPcvLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-cdk": { + "version": "2.92.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.92.0.tgz", + "integrity": "sha512-9aAWJvZWSBJQxcsDopXYUAm6/pGz6vOQy2zfkn+YBuBkNelvW+ok15KPY4xn5m76tYnN79W03Gnfp/nxZUlcww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.92.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.92.0.tgz", + "integrity": "sha512-J+SUFSnOt9u2GbY5QIABgjGNiw8bL/v0S3zsPhhO1dVwK+G7oE+bhLcAi3iILrw2sIpirNWH9K3W0by9K+cyMw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.200", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.1.1", + "ignore": "^5.2.4", + "jsonschema": "^1.4.1", + "minimatch": "^3.1.2", + "punycode": "^2.3.0", + "semver": "^7.5.4", + "table": "^6.8.1", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.12.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.2.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/lru-cache": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.5.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.8.1", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/uri-js": { + "version": "4.4.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.197", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", + "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/cdk/package.json b/cdk/package.json new file mode 100644 index 00000000..f1e769db --- /dev/null +++ b/cdk/package.json @@ -0,0 +1,27 @@ +{ + "name": "cdk", + "version": "0.1.0", + "bin": { + "cdk": "bin/cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.3", + "@types/node": "20.4.10", + "jest": "^29.6.2", + "ts-jest": "^29.1.1", + "aws-cdk": "2.92.0", + "ts-node": "^10.9.1", + "typescript": "~5.1.6" + }, + "dependencies": { + "aws-cdk-lib": "2.92.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/cdk/test/cdk.test.ts b/cdk/test/cdk.test.ts new file mode 100644 index 00000000..1e6b29c8 --- /dev/null +++ b/cdk/test/cdk.test.ts @@ -0,0 +1,17 @@ +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as Cdk from '../lib/cdk-stack'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/cdk-stack.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// // WHEN +// const stack = new Cdk.CdkStack(app, 'MyTestStack'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/cdk/tsconfig.json b/cdk/tsconfig.json new file mode 100644 index 00000000..aaa7dc51 --- /dev/null +++ b/cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From df36ec12066abdd763c79b19f89f7307d8915435 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 13:35:33 -0700 Subject: [PATCH 02/36] add basic S3EC implementation --- .gitignore | 12 + README.md | 17 +- SUPPORT_POLICY.rst | 29 ++ poetry.lock | 454 ++++++++++++++++++ pyproject.toml | 22 + src/s3_encryption/__init__.py | 84 ++++ src/s3_encryption/exceptions.py | 4 + src/s3_encryption/materials/__init__.py | 17 + .../materials/crypto_materials_manager.py | 62 +++ .../materials/encrypted_data_key.py | 20 + src/s3_encryption/materials/keyring.py | 100 ++++ src/s3_encryption/materials/kms_keyring.py | 117 +++++ src/s3_encryption/materials/materials.py | 59 +++ src/s3_encryption/metadata.py | 115 +++++ src/s3_encryption/pipelines.py | 150 ++++++ test/__init__.py | 2 + test/integration/__init__.py | 2 + test/integration/test_i_s3_encryption.py | 41 ++ test/test_encryption_materials.py | 57 +++ test/test_encryption_materials_integration.py | 85 ++++ test/test_metadata.py | 86 ++++ 21 files changed, 1519 insertions(+), 16 deletions(-) create mode 100644 .gitignore create mode 100644 SUPPORT_POLICY.rst create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/s3_encryption/__init__.py create mode 100644 src/s3_encryption/exceptions.py create mode 100644 src/s3_encryption/materials/__init__.py create mode 100644 src/s3_encryption/materials/crypto_materials_manager.py create mode 100644 src/s3_encryption/materials/encrypted_data_key.py create mode 100644 src/s3_encryption/materials/keyring.py create mode 100644 src/s3_encryption/materials/kms_keyring.py create mode 100644 src/s3_encryption/materials/materials.py create mode 100644 src/s3_encryption/metadata.py create mode 100644 src/s3_encryption/pipelines.py create mode 100644 test/__init__.py create mode 100644 test/integration/__init__.py create mode 100644 test/integration/test_i_s3_encryption.py create mode 100644 test/test_encryption_materials.py create mode 100644 test/test_encryption_materials_integration.py create mode 100644 test/test_metadata.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4b1c0c91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.idea +.vscode +# Exclude all pycache directories and bytecode +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Distribution / packaging +dist/ +build/ +*.egg-info/ diff --git a/README.md b/README.md index 847260ca..2104b640 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,2 @@ -## My Project - -TODO: Fill this README out! - -Be sure to: - -* Change the title in this README -* Edit your repository description on GitHub - -## Security - -See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. - -## License - -This project is licensed under the Apache-2.0 License. +# Amazon S3 Encryption Client Python diff --git a/SUPPORT_POLICY.rst b/SUPPORT_POLICY.rst new file mode 100644 index 00000000..3eafd39b --- /dev/null +++ b/SUPPORT_POLICY.rst @@ -0,0 +1,29 @@ +Overview +======== +This page describes the support policy for the Amazon S3 Encryption Client. We regularly provide the Amazon S3 Encryption Client with updates that may contain support for new or updated APIs, new features, enhancements, bug fixes, security patches, or documentation updates. Updates may also address changes with dependencies, language runtimes, and operating systems. + +We recommend users to stay up-to-date with Amazon S3 Encryption Client releases to keep up with the latest features, security updates, and underlying dependencies. Continued use of an unsupported client version is not recommended and is done at the user’s discretion + + +Major Version Lifecycle +======================== +The Amazon S3 Encryption Client follows the same major version lifecycle as the AWS SDK. For details on this lifecycle, see `AWS SDKs and Tools Maintenance Policy`_. + +Version Support Matrix +====================== +This table describes the current support status of each major version of the Amazon S3 Encryption Client for Python. It also shows the next status each major version will transition to, and the date at which that transition will happen. + +.. list-table:: + :widths: 30 50 50 50 + :header-rows: 1 + + * - Major version + - Current status + - Next status + - Next status date + * - 3.x + - Pre-Release + - Generally Available + - + +.. _AWS SDKs and Tools Maintenance Policy: https://docs.aws.amazon.com/sdkref/latest/guide/maint-policy.html#version-life-cycle diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..1ab72864 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,454 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "aws-cryptographic-material-providers" +version = "1.11.0" +description = "AWS Cryptographic Material Providers Library for Python" +optional = false +python-versions = "<4.0.0,>=3.11.0" +files = [ + {file = "aws_cryptographic_material_providers-1.11.0-py3-none-any.whl", hash = "sha256:9a9f0dca5b1902a4f16fb91cc1010dee74a721f84f411e81ffb4481fc0dd095f"}, + {file = "aws_cryptographic_material_providers-1.11.0.tar.gz", hash = "sha256:4ea5f9e5cc003e97d2ef98079dc25d8c49a0db01315ee887d19fd2f1c85ae9c3"}, +] + +[package.dependencies] +aws-cryptography-internal-dynamodb = "1.11.0" +aws-cryptography-internal-kms = "1.11.0" +aws-cryptography-internal-primitives = "1.11.0" +aws-cryptography-internal-standard-library = "1.11.0" + +[[package]] +name = "aws-cryptography-internal-dynamodb" +version = "1.11.0" +description = "" +optional = false +python-versions = "<4.0.0,>=3.11.0" +files = [ + {file = "aws_cryptography_internal_dynamodb-1.11.0-py3-none-any.whl", hash = "sha256:5a2da0ae6829d725f24018d001f4c733605f213820b723b6c75015843dc2427c"}, + {file = "aws_cryptography_internal_dynamodb-1.11.0.tar.gz", hash = "sha256:0800921ebb5dafc2853a2f5449f74aa03d24acd9ddb2ee58edca4002b97a5da5"}, +] + +[package.dependencies] +aws-cryptography-internal-standard-library = "1.11.0" +boto3 = ">=1.35.42,<2.0.0" + +[[package]] +name = "aws-cryptography-internal-kms" +version = "1.11.0" +description = "" +optional = false +python-versions = "<4.0.0,>=3.11.0" +files = [ + {file = "aws_cryptography_internal_kms-1.11.0-py3-none-any.whl", hash = "sha256:1c23cc8e970252fc7627868fc6b7a002400ec1d555ac29368e0eaddcceb07953"}, + {file = "aws_cryptography_internal_kms-1.11.0.tar.gz", hash = "sha256:a3ff5105b3e1c9d81e9698e0efc80de8a6bb8078b4512f9b39ed0f6161aae172"}, +] + +[package.dependencies] +aws-cryptography-internal-standard-library = "1.11.0" +boto3 = ">=1.35.42,<2.0.0" + +[[package]] +name = "aws-cryptography-internal-primitives" +version = "1.11.0" +description = "" +optional = false +python-versions = "<4.0.0,>=3.11.0" +files = [ + {file = "aws_cryptography_internal_primitives-1.11.0-py3-none-any.whl", hash = "sha256:84200885113f3534f4bff819ac1603c6d5c3bdd4d5c83a1b73ac2462cecec49b"}, + {file = "aws_cryptography_internal_primitives-1.11.0.tar.gz", hash = "sha256:9072af2c403b9e729dc767b44d1d642fa924a317a5bdbdffdf6dba0e93dc7996"}, +] + +[package.dependencies] +aws-cryptography-internal-standard-library = "1.11.0" +cryptography = ">=43.0.1,<46" + +[[package]] +name = "aws-cryptography-internal-standard-library" +version = "1.11.0" +description = "" +optional = false +python-versions = "<4.0.0,>=3.11.0" +files = [ + {file = "aws_cryptography_internal_standard_library-1.11.0-py3-none-any.whl", hash = "sha256:a2d5a4d8f70bce7242e8ebe06742223b8cd93253ed8081f44d7a8c1a086871e1"}, + {file = "aws_cryptography_internal_standard_library-1.11.0.tar.gz", hash = "sha256:36d82c6bc0361cf0ec3b7181804d375718f5c297949ddd902670f4452ecad3b0"}, +] + +[package.dependencies] +DafnyRuntimePython = "4.9.0" +pytz = ">=2023.3.post1,<2025.0.0" + +[[package]] +name = "boto3" +version = "1.39.14" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "boto3-1.39.14-py3-none-any.whl", hash = "sha256:82c6868cad18c3bd4170915e9525f9af5f83e9779c528417f8863629558fc2d0"}, + {file = "boto3-1.39.14.tar.gz", hash = "sha256:fabb16360a93b449d5241006485bcc761c26694e75ac01009f4459f114acc06e"}, +] + +[package.dependencies] +botocore = ">=1.39.14,<1.40.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.13.0,<0.14.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.39.14" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.9" +files = [ + {file = "botocore-1.39.14-py3-none-any.whl", hash = "sha256:4ed551c77194167b7e8063f33059bc2f9b2ead0ed4ee33dc7857273648ed4349"}, + {file = "botocore-1.39.14.tar.gz", hash = "sha256:7fc44d4ad13b524e5d8a6296785776ef5898ac026ff74df9b35313831d507926"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.23.8)"] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dafnyruntimepython" +version = "4.9.0" +description = "Dafny runtime for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "DafnyRuntimePython-4.9.0-py3-none-any.whl", hash = "sha256:c9cdcf127f5b6a4c6c9cf69016b9486318c3a6600e7f03fcbc621f6a5398479c"}, + {file = "dafnyruntimepython-4.9.0.tar.gz", hash = "sha256:03a4c2dbbe45c13dc2c7dbefad01812367b3bb217a14b4b848d7e94ef5c08cee"}, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "s3transfer" +version = "0.13.1" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.9" +files = [ + {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"}, + {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"}, +] + +[package.dependencies] +botocore = ">=1.37.4,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "b1781f15e07cc26d093bb8ed243f85d126ac76a46954cc1d1ff29261f1db380c" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..fba01461 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.poetry] +name = "amazon-s3-encryption-client-python" +version = "0.1.0" +description = "This library provides an S3 client that supports client-side encryption." +authors = ["AWS Crypto Tools "] +license = "Apache-2.0" +readme = "README.md" +packages = [{include = "s3_encryption", from = "src"}] + +[tool.poetry.dependencies] +python = "^3.11" +boto3 = "^1.37.2" +# There is a newer version, but MPL wants this one. +cryptography = "^43.0.1" +aws-cryptographic-material-providers = "^1.7.4" +attrs = "^25.1.0" +pytest = "^8.4.1" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py new file mode 100644 index 00000000..99493f07 --- /dev/null +++ b/src/s3_encryption/__init__.py @@ -0,0 +1,84 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import io +from botocore.response import StreamingBody +from attrs import define, field +from .pipelines import PutEncryptedObjectPipeline, GetEncryptedObjectPipeline +from .materials.keyring import AbstractKeyring +from .materials.crypto_materials_manager import AbstractCryptoMaterialsManager, DefaultCryptoMaterialsManager +from .metadata import ObjectMetadata + + +@define +class S3EncryptionClientConfig(): + """ + Configuration object for the S3 Encryption Client + """ + keyring: AbstractKeyring + cmm: AbstractCryptoMaterialsManager = field() + @cmm.default + def _default_cmm_for_keyring(self): + return DefaultCryptoMaterialsManager(self.keyring) + + +@define +class S3EncryptionClient(): + wrapped_s3_client = field() + config: S3EncryptionClientConfig = field() + + # TODO: I don't know exactly how boto3 works, + # we maybe instead prefer only using kwargs? + # Do we need to provide specific arg overloads? + # TODO: rename Data-> Body to match boto + def put_object(self, Bucket, Key, Data, EncryptionContext=None, **kwargs): + # Create a pipeline for this operation + pipeline = PutEncryptedObjectPipeline(self.config.cmm) + + + # Encrypt the data using the pipeline + data_bytes = Data + # We probably just shouldn't support strings, use utf8 for now + if type(Data) == str: + data_bytes = Data.encode('utf-8') + encrypted_data, encryption_metadata = pipeline.encrypt(data_bytes, encryption_context=EncryptionContext) + + # Add encryption metadata to the request parameters + params = { + 'Bucket': Bucket, + 'Key': Key, + 'Body': encrypted_data, + **kwargs + } + + # Add encryption metadata to the parameters + if encryption_metadata: + # Merge any existing metadata with our encryption metadata + metadata = params.get('Metadata', {}) + metadata.update(encryption_metadata) + params['Metadata'] = metadata + + return self.wrapped_s3_client.put_object(**params) + + def get_object(self, EncryptionContext=None, **kwargs): + # try just straight kwargs + params = { + **kwargs + } + + # Get the encrypted object from S3 + response = self.wrapped_s3_client.get_object(**params) + + # Create a pipeline for this operation + pipeline = GetEncryptedObjectPipeline(self.config.cmm) + + # Decrypt the data using the pipeline + decrypted_data = pipeline.decrypt(response, EncryptionContext) #encrypted_data, encryption_metadata) + + # Create a new streaming body with the decrypted data + stream = io.BytesIO(decrypted_data) + streaming_body = StreamingBody(stream, len(decrypted_data)) + + # Update the response with the decrypted data + response['Body'] = streaming_body + + return response diff --git a/src/s3_encryption/exceptions.py b/src/s3_encryption/exceptions.py new file mode 100644 index 00000000..034b2bdf --- /dev/null +++ b/src/s3_encryption/exceptions.py @@ -0,0 +1,4 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +class S3EncryptionClientError(Exception): + """Exception class for S3 Encryption Client errors.""" diff --git a/src/s3_encryption/materials/__init__.py b/src/s3_encryption/materials/__init__.py new file mode 100644 index 00000000..79178052 --- /dev/null +++ b/src/s3_encryption/materials/__init__.py @@ -0,0 +1,17 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from .keyring import AbstractKeyring +from .kms_keyring import KmsKeyring +from .crypto_materials_manager import AbstractCryptoMaterialsManager, DefaultCryptoMaterialsManager +from .encrypted_data_key import EncryptedDataKey +from .materials import EncryptionMaterials + +__all__ = [ + 'AbstractKeyring', + 'KmsKeyring', + 'AbstractCryptoMaterialsManager', + 'DefaultCryptoMaterialsManager', + 'EncryptedDataKey', + 'EncryptionMaterials' +] diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py new file mode 100644 index 00000000..33910d75 --- /dev/null +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -0,0 +1,62 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from attrs import define +from .keyring import AbstractKeyring +from .materials import EncryptionMaterials +from typing import List, Dict, Any + +# API Stub for CMM +class AbstractCryptoMaterialsManager(): + def getEncryptionMaterials(self, encMatsRequest): + """ + Get encryption materials from the keyring. + + Args: + encMatsRequest (Dict[str, Any] or EncryptionMaterials): Request containing encryption parameters + + Returns: + EncryptionMaterials: The encryption materials + """ + raise NotImplementedError + + def decryptMaterials(self, decMatsRequest): + """ + Decrypt materials using the keyring. + + Args: + decMatsRequest (Dict[str, Any]): Request containing decryption parameters + + Returns: + Dict[str, Any]: The decryption materials + """ + raise NotImplementedError + +@define +class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): + keyring: AbstractKeyring + + def getEncryptionMaterials(self, encMatsRequest): + """ + Get encryption materials from the keyring. + + Args: + encMatsRequest (Dict[str, Any]): Request containing encryption parameters + + Returns: + EncryptionMaterials: The encryption materials + """ + # Convert dictionary to EncryptionMaterials if needed + if isinstance(encMatsRequest, dict): + materials = EncryptionMaterials( + encryption_context=encMatsRequest.get('encryption_context', {}) + ) + else: + materials = encMatsRequest + + return self.keyring.onEncrypt(materials) + + def decryptMaterials(self, decMatsRequest): + # TODO: Fill with defaults + stuff from decMatsRequest + materials = {**decMatsRequest} + encrypted_data_keys = decMatsRequest.get('encrypted_data_keys') + return self.keyring.onDecrypt(materials, encrypted_data_keys) diff --git a/src/s3_encryption/materials/encrypted_data_key.py b/src/s3_encryption/materials/encrypted_data_key.py new file mode 100644 index 00000000..72fbeae2 --- /dev/null +++ b/src/s3_encryption/materials/encrypted_data_key.py @@ -0,0 +1,20 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from attrs import define, field + +@define +class EncryptedDataKey: + """ + Class representing an encrypted data key. + + An encrypted data key contains information about the key provider + and the encrypted data key itself. + + Attributes: + key_provider_info (str): Information about the key provider + key_provider_id (bytes): Identifier for the key provider + encrypted_data_key (bytes): The encrypted data key + """ + key_provider_info: str = field() + key_provider_id: bytes = field() + encrypted_data_key: bytes = field() diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py new file mode 100644 index 00000000..78a884e9 --- /dev/null +++ b/src/s3_encryption/materials/keyring.py @@ -0,0 +1,100 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from attrs import define, field +from ..exceptions import S3EncryptionClientError +from .materials import EncryptionMaterials + +@define +class AbstractKeyring(): + # Ideally, all keyrings would inherit this field. + # However, attrs doesn't allow us to set a default here, + # when inheriting keyrings have optional fields. + # Even without a default it doesn't seem to play nice with attrs. + #enableLegacyWrappingAlgorithms: bool = field(default=False) + + def onEncrypt(self, encMaterials): + """ + Process encryption materials. + + Args: + encMaterials (EncryptionMaterials): Encryption materials to process + + Returns: + EncryptionMaterials: The processed encryption materials + """ + raise NotImplementedError + + def onDecrypt(self, decMaterials, encrypted_data_keys=None): + """ + Decrypt one of the encrypted data keys and update decMaterials. + + Args: + decMaterials (dict): A dictionary containing decryption materials + encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. + + Returns: + dict: The updated decMaterials with the plaintext data key (PDK) + """ + raise NotImplementedError + + +@define +class S3Keyring(AbstractKeyring): + """ + Base class for S3 encryption keyrings that provides common validation logic. + """ + # Ideally this would be set, but attrs doesn't play nice + # enable_legacy_wrapping_algorithms: bool = field(default=False) + + def onEncrypt(self, encMaterials): + """ + Validate encryption materials before encryption. + + Args: + encMaterials (EncryptionMaterials or dict): Encryption materials + + Returns: + EncryptionMaterials: The validated encryption materials + """ + # Convert dict to EncryptionMaterials if needed + if isinstance(encMaterials, dict): + encMaterials = EncryptionMaterials.from_dict(encMaterials) + + # Validate encryption materials + if not isinstance(encMaterials, EncryptionMaterials): + raise S3EncryptionClientError("Encryption materials must be an EncryptionMaterials instance or a dictionary") + + # Ensure encryption_context is a dictionary + if not isinstance(encMaterials.encryption_context, dict): + raise S3EncryptionClientError("Encryption context must be a dictionary") + + return encMaterials + + def onDecrypt(self, decMaterials, encrypted_data_keys=None): + """ + Validate decryption materials before decryption. + + Args: + decMaterials (dict): A dictionary containing decryption materials + encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. + + Returns: + dict: The validated decryption materials + """ + # Validate decryption materials + if not isinstance(decMaterials, dict): + raise S3EncryptionClientError("Decryption materials must be a dictionary") + + # Validate encrypted_data_keys + if encrypted_data_keys is None or len(encrypted_data_keys) == 0: + raise S3EncryptionClientError("No encrypted data keys provided") + + # Ensure encryption contexts are dictionaries if present + if 'encryption_context_from_request' in decMaterials and not isinstance(decMaterials['encryption_context_from_request'], dict): + raise S3EncryptionClientError("Encryption context from request must be a dictionary") + + if 'encryption_context_stored' in decMaterials and not isinstance(decMaterials['encryption_context_stored'], dict): + raise S3EncryptionClientError("Stored encryption context must be a dictionary") + + return decMaterials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py new file mode 100644 index 00000000..69f10700 --- /dev/null +++ b/src/s3_encryption/materials/kms_keyring.py @@ -0,0 +1,117 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from .keyring import S3Keyring +from .encrypted_data_key import EncryptedDataKey +from .materials import EncryptionMaterials +from ..exceptions import S3EncryptionClientError +from attrs import define, field + +KMS_CONTEXT_DEFAULT_KEY = "aws:x-amz-cek-alg" +KMS_V1_DEFAULT_KEY = "kms_cmk_id" + +@define +class KmsKeyring(S3Keyring): + kms_client = field() + kms_key_id: str = field() + enable_legacy_wrapping_algorithms: bool = field(default=False) + + def onEncrypt(self, encMaterials): + """ + Process encryption materials using KMS. + + Args: + encMaterials (EncryptionMaterials): Encryption materials to process + + Returns: + EncryptionMaterials: The processed encryption materials with KMS-generated keys + """ + try: + # Call parent class validation + encMaterials = super().onEncrypt(encMaterials) + + # Add default encryption context + encryption_context = encMaterials.encryption_context + encryption_context["aws:x-amz-cek-alg"] = "AES/GCM/NoPadding" + + response = self.kms_client.generate_data_key( + KeyId = self.kms_key_id, + KeySpec = 'AES_256', + EncryptionContext = encryption_context + ) + # Create an EncryptedDataKey instance + encrypted_data_key = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=response['CiphertextBlob'] + ) + encMaterials.encrypted_data_key = encrypted_data_key + encMaterials.plaintext_data_key = response['Plaintext'] + return encMaterials + except Exception as e: + raise + + def onDecrypt(self, decMaterials, encrypted_data_keys=None): + """ + Decrypt one of the encrypted data keys and update decMaterials. + + Args: + decMaterials (dict): A dictionary containing decryption materials + encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. + + Returns: + dict: The updated decMaterials with the plaintext data key (PDK) + """ + try: + # Call parent class validation + decMaterials = super().onDecrypt(decMaterials, encrypted_data_keys) + + # Handle both single EDK (backward compatibility) and list of EDKs + edks = encrypted_data_keys + + # Try to decrypt each EDK until one succeeds + # TODO: probably just enforce |EDKs| == 1 and remove loop + last_exception = None + for edk in edks: + try: + edk_bytes = edk.encrypted_data_key + if edk.key_provider_info == "kms+context": + encryption_context_from_request = decMaterials.get('encryption_context_from_request', {}) + encryption_context_stored = decMaterials.get('encryption_context_stored', {}) + + # Default EC MUST NOT be passed in via request + if KMS_CONTEXT_DEFAULT_KEY in encryption_context_from_request: + raise S3EncryptionClientError(f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the S3 encryption client") + + # The stored EC, minus default key/values, MUST match provided EC + encryption_context_stored_copy = encryption_context_stored.copy() + encryption_context_stored_copy.pop(KMS_V1_DEFAULT_KEY, None) + encryption_context_stored_copy.pop(KMS_CONTEXT_DEFAULT_KEY, None) + if encryption_context_stored_copy != encryption_context_from_request: + # TODO: modeled error + raise S3EncryptionClientError("Provided encryption context does not match information retrieved from S3") + + # Update decMaterials with the modified encryption context + elif edk.key_provider_info == "kms": + if not self.enable_legacy_wrapping_algorithms: + raise S3EncryptionClientError(f"Enable legacy wrapping algorithms to use legacy key wrapping algorithm: {edk.key_provider_info}") + else: + raise S3EncryptionClientError(f"{edk.key_provider_info} is not a valid key wrapping algorithm!") + + response = self.kms_client.decrypt( + KeyId = self.kms_key_id, + CiphertextBlob = edk_bytes, + EncryptionContext = decMaterials['encryption_context_stored'] + ) + decMaterials['PDK'] = response['Plaintext'] + return decMaterials + except Exception as e: + last_exception = e + continue + + # If we get here, none of the EDKs could be decrypted + if last_exception: + raise last_exception + else: + raise S3EncryptionClientError("Failed to decrypt any of the encrypted data keys") + except Exception as e: + raise diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py new file mode 100644 index 00000000..32b0b880 --- /dev/null +++ b/src/s3_encryption/materials/materials.py @@ -0,0 +1,59 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from attrs import define, field +from typing import Optional, Dict, Any +from .encrypted_data_key import EncryptedDataKey + +@define +class EncryptionMaterials: + """ + Class representing encryption materials for S3 encryption. + + This class provides a structured way to handle encryption materials + with fields corresponding to the data needed for encryption operations. + + Attributes: + encryption_context (Dict[str, str]): Context information for encryption + encrypted_data_key (Optional[EncryptedDataKey]): The encrypted data key + plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) + """ + encryption_context: Dict[str, str] = field(factory=dict) + encrypted_data_key: Optional[EncryptedDataKey] = field(default=None) + plaintext_data_key: Optional[bytes] = field(default=None) + + @classmethod + def from_dict(cls, materials_dict: Dict[str, Any]) -> 'EncryptionMaterials': + """ + Create an EncryptionMaterials instance from a dictionary. + + Args: + materials_dict (Dict[str, Any]): Dictionary containing encryption materials + + Returns: + EncryptionMaterials: A new instance with fields populated from the dictionary + """ + return cls( + encryption_context=materials_dict.get('encryption_context', {}), + encrypted_data_key=materials_dict.get('encrypted_data_key'), + plaintext_data_key=materials_dict.get('PDK') + ) + + def to_dict(self) -> Dict[str, Any]: + """ + Convert the EncryptionMaterials instance to a dictionary. + + Returns: + Dict[str, Any]: Dictionary containing encryption materials + """ + result = {} + + if self.encryption_context: + result['encryption_context'] = self.encryption_context + + if self.encrypted_data_key is not None: + result['encrypted_data_key'] = self.encrypted_data_key + + if self.plaintext_data_key is not None: + result['PDK'] = self.plaintext_data_key + + return result diff --git a/src/s3_encryption/metadata.py b/src/s3_encryption/metadata.py new file mode 100644 index 00000000..4894792a --- /dev/null +++ b/src/s3_encryption/metadata.py @@ -0,0 +1,115 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json +from attrs import define, field +from typing import Optional, Dict, Any + + +@define +class ObjectMetadata: + """ + Class representing metadata for encrypted S3 objects. + + This class provides a structured way to handle encryption metadata + with fields corresponding to standard S3 encryption headers. + + All fields are optional and correspond to the following S3 encryption headers: + - encrypted_data_key_v1: The encrypted data key (legacy format) + - encrypted_data_key_v2: The encrypted data key (current format) + - encrypted_data_key_algorithm: The algorithm used to encrypt the data key (e.g. AES/GCM or kms+context) + - encrypted_data_key_context: The encryption context used for the data key + - content_iv: The initialization vector used for content encryption + - content_cipher: The cipher algorithm used for content encryption (e.g. AES/GCM/NoPadding) + - content_cipher_tag_length: The length of the authentication tag + - instruction_file: Marker for instruction files + """ + # The encrypted data key (legacy format) + encrypted_data_key_v1: Optional[str] = field(default=None) + # The encrypted data key (current format) + encrypted_data_key_v2: Optional[str] = field(default=None) + # The algorithm used to encrypt the data key (e.g. AES/GCM or kms+context) + encrypted_data_key_algorithm: Optional[str] = field(default=None) + # The encryption context used for the data key + encrypted_data_key_context: Optional[dict] = field(default=None) + # The initialization vector used for content encryption + content_iv: Optional[str] = field(default=None) + # The cipher algorithm used for content encryption (e.g. AES/GCM/NoPadding) + content_cipher: Optional[str] = field(default=None) + # The length of the authentication tag + content_cipher_tag_length: Optional[str] = field(default="128") + # Marker for instruction files + instruction_file: Optional[str] = field(default=None) + + # Constants for metadata keys + ENCRYPTED_DATA_KEY_V1 = "x-amz-key" + ENCRYPTED_DATA_KEY_V2 = "x-amz-key-v2" + ENCRYPTED_DATA_KEY_ALGORITHM = "x-amz-wrap-alg" + ENCRYPTED_DATA_KEY_CONTEXT = "x-amz-matdesc" + CONTENT_IV = "x-amz-iv" + CONTENT_CIPHER = "x-amz-cek-alg" + CONTENT_CIPHER_TAG_LENGTH = "x-amz-tag-len" + INSTRUCTION_FILE = "x-amz-crypto-instr-file" + + @classmethod + def from_dict(cls, metadata_dict: Dict[str, Any]) -> 'ObjectMetadata': + """ + Create an ObjectMetadata instance from a dictionary. + + Args: + metadata_dict (Dict[str, Any]): Dictionary containing metadata keys and values + + Returns: + ObjectMetadata: A new instance with fields populated from the dictionary + """ + # Parse the encryption context if present + encryption_context = None + if cls.ENCRYPTED_DATA_KEY_CONTEXT in metadata_dict: + context_str = metadata_dict.get(cls.ENCRYPTED_DATA_KEY_CONTEXT) + if context_str is not None: + encryption_context = json.loads(context_str) + + return cls( + encrypted_data_key_v1=metadata_dict.get(cls.ENCRYPTED_DATA_KEY_V1), + encrypted_data_key_v2=metadata_dict.get(cls.ENCRYPTED_DATA_KEY_V2), + encrypted_data_key_algorithm=metadata_dict.get(cls.ENCRYPTED_DATA_KEY_ALGORITHM), + encrypted_data_key_context=encryption_context, + content_iv=metadata_dict.get(cls.CONTENT_IV), + content_cipher=metadata_dict.get(cls.CONTENT_CIPHER), + content_cipher_tag_length=metadata_dict.get(cls.CONTENT_CIPHER_TAG_LENGTH), + instruction_file=metadata_dict.get(cls.INSTRUCTION_FILE) + ) + + def to_dict(self) -> Dict[str, str]: + """ + Convert the ObjectMetadata instance to a dictionary. + + Returns: + Dict[str, str]: Dictionary containing non-None metadata values + """ + result = {} + + if self.encrypted_data_key_v1 is not None: + result[self.ENCRYPTED_DATA_KEY_V1] = self.encrypted_data_key_v1 + + if self.encrypted_data_key_v2 is not None: + result[self.ENCRYPTED_DATA_KEY_V2] = self.encrypted_data_key_v2 + + if self.encrypted_data_key_algorithm is not None: + result[self.ENCRYPTED_DATA_KEY_ALGORITHM] = self.encrypted_data_key_algorithm + + if self.encrypted_data_key_context is not None: + result[self.ENCRYPTED_DATA_KEY_CONTEXT] = json.dumps(self.encrypted_data_key_context) + + if self.content_iv is not None: + result[self.CONTENT_IV] = self.content_iv + + if self.content_cipher is not None: + result[self.CONTENT_CIPHER] = self.content_cipher + + if self.content_cipher_tag_length is not None: + result[self.CONTENT_CIPHER_TAG_LENGTH] = self.content_cipher_tag_length + + if self.instruction_file is not None: + result[self.INSTRUCTION_FILE] = self.instruction_file + + return result diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py new file mode 100644 index 00000000..7ddee40c --- /dev/null +++ b/src/s3_encryption/pipelines.py @@ -0,0 +1,150 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from attrs import define, field +import os +import base64 +from .materials.crypto_materials_manager import AbstractCryptoMaterialsManager +from .materials.encrypted_data_key import EncryptedDataKey +from .materials.materials import EncryptionMaterials +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from .metadata import ObjectMetadata + +@define +class PutEncryptedObjectPipeline: + """ + Pipeline for encrypting objects before they are put into S3. + + This pipeline handles only the encryption process for S3 objects. + The actual S3 API calls are handled by the S3EncryptionClient. + """ + cmm: AbstractCryptoMaterialsManager = field() + + def encrypt(self, plaintext, encryption_context=None): + """ + Encrypt the data before it is stored in S3. + + Args: + data (bytes or str): The data to be encrypted + encryption_context (dict, optional): Additional context for encryption + + Returns: + bytes: The encrypted data + dict: Metadata about the encryption to be stored with the object + """ + # Create encryption materials request with encryption context + enc_mats_request = EncryptionMaterials( + encryption_context={} if encryption_context is None else encryption_context + ) + + # Get encryption materials from the crypto materials manager + enc_mats = self.cmm.getEncryptionMaterials(enc_mats_request) + + # Generate initialization vector + iv = os.urandom(12) + + # Encrypt the data + if enc_mats.plaintext_data_key is None: + raise RuntimeError("No plaintext data key found!") + + aesgcm = AESGCM(enc_mats.plaintext_data_key) + ciphertext = aesgcm.encrypt( + nonce=iv, + data=plaintext, + associated_data=None + ) + encrypted_data = ciphertext + b64_iv = base64.b64encode(iv).decode('utf-8') + + # Get the encrypted data key + if enc_mats.encrypted_data_key is None: + raise RuntimeError("No encrypted data key found!") + + edk_bytes = enc_mats.encrypted_data_key.encrypted_data_key + b64_edk = base64.b64encode(edk_bytes).decode('utf-8') + + # Create metadata using the ObjectMetadata class + metadata = ObjectMetadata( + encrypted_data_key_v2=b64_edk, + encrypted_data_key_algorithm="kms+context", + content_iv=b64_iv, + content_cipher="AES/GCM/NoPadding", + encrypted_data_key_context=enc_mats.encryption_context + ) + + # Convert to dictionary for storage in S3 metadata + encryption_metadata = metadata.to_dict() + + return encrypted_data, encryption_metadata + + +@define +class GetEncryptedObjectPipeline: + """ + Pipeline for decrypting objects after they are retrieved from S3. + + This pipeline handles only the decryption process for S3 objects. + The actual S3 API calls are handled by the S3EncryptionClient. + """ + cmm: AbstractCryptoMaterialsManager = field() + + def decrypt(self, response, encryption_context={}): + """ + Decrypt the data after it is retrieved from S3. + + Args: + encrypted_data (bytes): The encrypted data retrieved from S3 + encryption_metadata (dict, optional): Metadata about the encryption + + Returns: + bytes or str: The decrypted data + """ + # Convert the metadata dictionary to an ObjectMetadata instance + encrypted_data = response.get('Body').read() + encryption_metadata = response.get('Metadata', {}) + metadata = ObjectMetadata.from_dict(encryption_metadata) + + iv_b64 = metadata.content_iv + edk_b64 = metadata.encrypted_data_key_v2 + + # TODO: probably move this to ObjectMetadata + iv_bytes = base64.b64decode(iv_b64) + + # Create a list of encrypted data keys to try + encrypted_data_keys = [] + # Create an instance of EncryptedDataKey + if edk_b64: + edk_bytes = base64.b64decode(edk_b64) + encrypted_data_key = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info=metadata.encrypted_data_key_algorithm, + encrypted_data_key=edk_bytes + ) + encrypted_data_keys.append(encrypted_data_key) + + # Also check for legacy encrypted data key (v1) if available + if metadata.encrypted_data_key_v1: + legacy_edk_bytes = base64.b64decode(metadata.encrypted_data_key_v1) + legacy_encrypted_data_key = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info=metadata.encrypted_data_key_algorithm, + encrypted_data_key=legacy_edk_bytes + ) + encrypted_data_keys.append(legacy_encrypted_data_key) + + dec_mat_req = { + "iv": iv_bytes, + "encrypted_data_keys": encrypted_data_keys, + "encryption_context_stored": metadata.encrypted_data_key_context, + "encryption_context_from_request": encryption_context + } + dec_mats = self.cmm.decryptMaterials(dec_mat_req) + + aesgcm = AESGCM(dec_mats['PDK']) + + plaintext = aesgcm.decrypt( + nonce=iv_bytes, + data=encrypted_data, + associated_data=None + ) + + return plaintext diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..f94fd12a --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,2 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/integration/__init__.py b/test/integration/__init__.py new file mode 100644 index 00000000..f94fd12a --- /dev/null +++ b/test/integration/__init__.py @@ -0,0 +1,2 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py new file mode 100644 index 00000000..981fbd3c --- /dev/null +++ b/test/integration/test_i_s3_encryption.py @@ -0,0 +1,41 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import boto3 +from datetime import datetime +from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig +from s3_encryption.materials.kms_keyring import KmsKeyring + +bucket = "s3-ec-python-v3-test" + +def test_simple_roundtrip(): + key = "simple-rt" + key += datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + + data = "test input for simple v3 round trip" + + kms_key_id = "arn:aws:kms:us-east-2:657301468084:key/1f469b1a-5cfa-4879-9bdf-27b3abd9b8d5" + kms_client = boto3.client("kms", region_name="us-east-2") + + keyring = KmsKeyring(kms_client, kms_key_id) + + wrapped_client = boto3.client("s3") + config = S3EncryptionClientConfig(keyring) + s3ec = S3EncryptionClient(wrapped_client, config) + s3ec.put_object(Bucket=bucket, Key=key, Data=data) + print("put object success!") + get_req = { + 'Bucket': bucket, + 'Key': key + } + response = s3ec.get_object(**get_req) + output = response['Body'].read().decode('utf-8') + print("get succeeded!") + print(response) + if output != data: + print("Uh oh! Input and output don't match!") + print("Input:") + print(input) + print("Output:") + print(output) + else: + print("Success!") \ No newline at end of file diff --git a/test/test_encryption_materials.py b/test/test_encryption_materials.py new file mode 100644 index 00000000..b1e6b653 --- /dev/null +++ b/test/test_encryption_materials.py @@ -0,0 +1,57 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from src.s3_encryption.materials.materials import EncryptionMaterials +from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey + +class TestEncryptionMaterials(unittest.TestCase): + def test_create_encryption_materials(self): + """Test creating an EncryptionMaterials instance.""" + materials = EncryptionMaterials() + self.assertEqual(materials.encryption_context, {}) + self.assertIsNone(materials.encrypted_data_key) + self.assertIsNone(materials.plaintext_data_key) + + def test_create_with_encryption_context(self): + """Test creating an EncryptionMaterials instance with an encryption context.""" + encryption_context = {"key1": "value1", "key2": "value2"} + materials = EncryptionMaterials(encryption_context=encryption_context) + self.assertEqual(materials.encryption_context, encryption_context) + + def test_from_dict(self): + """Test creating an EncryptionMaterials instance from a dictionary.""" + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + materials_dict = { + 'encryption_context': {"key1": "value1"}, + 'encrypted_data_key': edk, + 'PDK': b'plaintext-data-key' + } + materials = EncryptionMaterials.from_dict(materials_dict) + self.assertEqual(materials.encryption_context, {"key1": "value1"}) + self.assertEqual(materials.encrypted_data_key, edk) + self.assertEqual(materials.plaintext_data_key, b'plaintext-data-key') + + def test_to_dict(self): + """Test converting an EncryptionMaterials instance to a dictionary.""" + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + materials = EncryptionMaterials( + encryption_context={"key1": "value1"}, + encrypted_data_key=edk, + plaintext_data_key=b'plaintext-data-key' + ) + materials_dict = materials.to_dict() + self.assertEqual(materials_dict['encryption_context'], {"key1": "value1"}) + self.assertEqual(materials_dict['encrypted_data_key'], edk) + self.assertEqual(materials_dict['PDK'], b'plaintext-data-key') + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_encryption_materials_integration.py b/test/test_encryption_materials_integration.py new file mode 100644 index 00000000..7082bb8f --- /dev/null +++ b/test/test_encryption_materials_integration.py @@ -0,0 +1,85 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from unittest.mock import MagicMock, patch +from src.s3_encryption.materials.materials import EncryptionMaterials +from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey +from src.s3_encryption.materials.keyring import S3Keyring +from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager + +class TestEncryptionMaterialsIntegration(unittest.TestCase): + def test_keyring_onEncrypt(self): + """Test that S3Keyring.onEncrypt properly handles EncryptionMaterials.""" + # Create a keyring + keyring = S3Keyring() + + # Create encryption materials + materials = EncryptionMaterials( + encryption_context={"key1": "value1"} + ) + + # Call onEncrypt + result = keyring.onEncrypt(materials) + + # Verify the result is an EncryptionMaterials instance + self.assertIsInstance(result, EncryptionMaterials) + self.assertEqual(result.encryption_context, {"key1": "value1"}) + + def test_cmm_getEncryptionMaterials_with_dict(self): + """Test that DefaultCryptoMaterialsManager.getEncryptionMaterials properly handles dictionary input.""" + # Create a mock keyring + keyring = MagicMock() + keyring.onEncrypt.return_value = EncryptionMaterials( + encryption_context={"key1": "value1"}, + encrypted_data_key=EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ), + plaintext_data_key=b'plaintext-data-key' + ) + + # Create a CMM + cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + # Call getEncryptionMaterials with a dictionary + result = cmm.getEncryptionMaterials({"encryption_context": {"key1": "value1"}}) + + # Verify the result is an EncryptionMaterials instance + self.assertIsInstance(result, EncryptionMaterials) + self.assertEqual(result.encryption_context, {"key1": "value1"}) + self.assertIsNotNone(result.encrypted_data_key) + self.assertIsNotNone(result.plaintext_data_key) + + def test_cmm_getEncryptionMaterials_with_materials(self): + """Test that DefaultCryptoMaterialsManager.getEncryptionMaterials properly handles EncryptionMaterials input.""" + # Create a mock keyring + keyring = MagicMock() + keyring.onEncrypt.return_value = EncryptionMaterials( + encryption_context={"key1": "value1"}, + encrypted_data_key=EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ), + plaintext_data_key=b'plaintext-data-key' + ) + + # Create a CMM + cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + # Call getEncryptionMaterials with an EncryptionMaterials instance + materials = EncryptionMaterials( + encryption_context={"key1": "value1"} + ) + result = cmm.getEncryptionMaterials(materials) + + # Verify the result is an EncryptionMaterials instance + self.assertIsInstance(result, EncryptionMaterials) + self.assertEqual(result.encryption_context, {"key1": "value1"}) + self.assertIsNotNone(result.encrypted_data_key) + self.assertIsNotNone(result.plaintext_data_key) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_metadata.py b/test/test_metadata.py new file mode 100644 index 00000000..40a813aa --- /dev/null +++ b/test/test_metadata.py @@ -0,0 +1,86 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import unittest +import sys +import os + +# Add the src directory to the Python path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from s3_encryption.metadata import ObjectMetadata + + +class TestObjectMetadata(unittest.TestCase): + def test_from_dict(self): + # Create a metadata dictionary + metadata_dict = { + "x-amz-key-v2": "encrypted-key-data", + "x-amz-wrap-alg": "kms+context", + "x-amz-iv": "base64-encoded-iv", + "x-amz-cek-alg": "AES/GCM/NoPadding" + } + + # Create an ObjectMetadata instance from the dictionary + metadata = ObjectMetadata.from_dict(metadata_dict) + + # Verify that the fields were populated correctly + self.assertEqual(metadata.encrypted_data_key_v2, "encrypted-key-data") + self.assertEqual(metadata.encrypted_data_key_algorithm, "kms+context") + self.assertEqual(metadata.content_iv, "base64-encoded-iv") + self.assertEqual(metadata.content_cipher, "AES/GCM/NoPadding") + + # Verify that fields not in the dictionary are None + self.assertIsNone(metadata.encrypted_data_key_v1) + self.assertIsNone(metadata.encrypted_data_key_context) + # Note: content_cipher_tag_length is None because it's not in the input dictionary + self.assertIsNone(metadata.content_cipher_tag_length) + self.assertIsNone(metadata.instruction_file) + + def test_to_dict(self): + # Create an ObjectMetadata instance with some fields set + metadata = ObjectMetadata( + encrypted_data_key_v2="encrypted-key-data", + encrypted_data_key_algorithm="kms+context", + content_iv="base64-encoded-iv", + content_cipher="AES/GCM/NoPadding" + ) + + # Convert to dictionary + metadata_dict = metadata.to_dict() + + # Verify that the dictionary contains the expected keys and values + self.assertEqual(metadata_dict["x-amz-key-v2"], "encrypted-key-data") + self.assertEqual(metadata_dict["x-amz-wrap-alg"], "kms+context") + self.assertEqual(metadata_dict["x-amz-iv"], "base64-encoded-iv") + self.assertEqual(metadata_dict["x-amz-cek-alg"], "AES/GCM/NoPadding") + + # Verify that fields that are None are not included in the dictionary + self.assertNotIn("x-amz-key", metadata_dict) + self.assertNotIn("x-amz-matdesc", metadata_dict) + # Note: content_cipher_tag_length has a default value of "128" + self.assertEqual(metadata_dict.get("x-amz-tag-len"), "128") + self.assertNotIn("x-amz-crypto-instr-file", metadata_dict) + + def test_roundtrip(self): + # Create a metadata dictionary + original_dict = { + "x-amz-key-v2": "encrypted-key-data", + "x-amz-wrap-alg": "kms+context", + "x-amz-iv": "base64-encoded-iv", + "x-amz-cek-alg": "AES/GCM/NoPadding" + } + + # Convert to ObjectMetadata and back to dictionary + metadata = ObjectMetadata.from_dict(original_dict) + result_dict = metadata.to_dict() + + # Remove the tag length field which has a default value + if "x-amz-tag-len" in result_dict: + result_dict.pop("x-amz-tag-len") + + # Verify that the result matches the original + self.assertEqual(result_dict, original_dict) + + +if __name__ == "__main__": + unittest.main() From 62edf8a70440a44f329c482e6bcdf639ebd20b8f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 15:11:30 -0700 Subject: [PATCH 03/36] include decryptionMaterials change --- .../materials/crypto_materials_manager.py | 27 ++-- src/s3_encryption/materials/keyring.py | 26 ++-- src/s3_encryption/materials/kms_keyring.py | 19 +-- src/s3_encryption/materials/materials.py | 69 +++++++++- src/s3_encryption/pipelines.py | 28 +++-- test/test_decryption_materials.py | 89 +++++++++++++ test/test_decryption_materials_integration.py | 118 ++++++++++++++++++ 7 files changed, 336 insertions(+), 40 deletions(-) create mode 100644 test/test_decryption_materials.py create mode 100644 test/test_decryption_materials_integration.py diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index 33910d75..f30f3491 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 from attrs import define from .keyring import AbstractKeyring -from .materials import EncryptionMaterials -from typing import List, Dict, Any +from .materials import EncryptionMaterials, DecryptionMaterials +from typing import List, Dict, Any, Union # API Stub for CMM class AbstractCryptoMaterialsManager(): @@ -24,10 +24,10 @@ def decryptMaterials(self, decMatsRequest): Decrypt materials using the keyring. Args: - decMatsRequest (Dict[str, Any]): Request containing decryption parameters + decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters Returns: - Dict[str, Any]: The decryption materials + DecryptionMaterials: The decryption materials """ raise NotImplementedError @@ -56,7 +56,20 @@ def getEncryptionMaterials(self, encMatsRequest): return self.keyring.onEncrypt(materials) def decryptMaterials(self, decMatsRequest): - # TODO: Fill with defaults + stuff from decMatsRequest - materials = {**decMatsRequest} - encrypted_data_keys = decMatsRequest.get('encrypted_data_keys') + """ + Decrypt materials using the keyring. + + Args: + decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters + + Returns: + DecryptionMaterials: The decryption materials + """ + # Convert dictionary to DecryptionMaterials if needed + if isinstance(decMatsRequest, dict): + materials = DecryptionMaterials.from_dict(decMatsRequest) + else: + materials = decMatsRequest + + encrypted_data_keys = materials.encrypted_data_keys return self.keyring.onDecrypt(materials, encrypted_data_keys) diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 78a884e9..222eec7b 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -3,7 +3,8 @@ from attrs import define, field from ..exceptions import S3EncryptionClientError -from .materials import EncryptionMaterials +from .materials import EncryptionMaterials, DecryptionMaterials +from typing import List, Optional @define class AbstractKeyring(): @@ -30,11 +31,11 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): Decrypt one of the encrypted data keys and update decMaterials. Args: - decMaterials (dict): A dictionary containing decryption materials + decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. Returns: - dict: The updated decMaterials with the plaintext data key (PDK) + DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) """ raise NotImplementedError @@ -76,25 +77,28 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): Validate decryption materials before decryption. Args: - decMaterials (dict): A dictionary containing decryption materials + decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. Returns: - dict: The validated decryption materials + DecryptionMaterials: The validated decryption materials """ # Validate decryption materials - if not isinstance(decMaterials, dict): - raise S3EncryptionClientError("Decryption materials must be a dictionary") + if not isinstance(decMaterials, DecryptionMaterials): + raise S3EncryptionClientError("Decryption materials must be a DecryptionMaterials instance") + + # Use encrypted_data_keys from parameters if provided, otherwise use from decMaterials + edks = encrypted_data_keys if encrypted_data_keys is not None else decMaterials.encrypted_data_keys # Validate encrypted_data_keys - if encrypted_data_keys is None or len(encrypted_data_keys) == 0: + if edks is None or len(edks) == 0: raise S3EncryptionClientError("No encrypted data keys provided") - # Ensure encryption contexts are dictionaries if present - if 'encryption_context_from_request' in decMaterials and not isinstance(decMaterials['encryption_context_from_request'], dict): + # Ensure encryption contexts are dictionaries + if not isinstance(decMaterials.encryption_context_from_request, dict): raise S3EncryptionClientError("Encryption context from request must be a dictionary") - if 'encryption_context_stored' in decMaterials and not isinstance(decMaterials['encryption_context_stored'], dict): + if not isinstance(decMaterials.encryption_context_stored, dict): raise S3EncryptionClientError("Stored encryption context must be a dictionary") return decMaterials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 69f10700..75d75a27 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -2,9 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 from .keyring import S3Keyring from .encrypted_data_key import EncryptedDataKey -from .materials import EncryptionMaterials +from .materials import EncryptionMaterials, DecryptionMaterials from ..exceptions import S3EncryptionClientError from attrs import define, field +from typing import List, Optional KMS_CONTEXT_DEFAULT_KEY = "aws:x-amz-cek-alg" KMS_V1_DEFAULT_KEY = "kms_cmk_id" @@ -55,18 +56,18 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): Decrypt one of the encrypted data keys and update decMaterials. Args: - decMaterials (dict): A dictionary containing decryption materials + decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. Returns: - dict: The updated decMaterials with the plaintext data key (PDK) + DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) """ try: # Call parent class validation decMaterials = super().onDecrypt(decMaterials, encrypted_data_keys) - # Handle both single EDK (backward compatibility) and list of EDKs - edks = encrypted_data_keys + # Use encrypted_data_keys from parameters if provided, otherwise use from decMaterials + edks = encrypted_data_keys if encrypted_data_keys is not None else decMaterials.encrypted_data_keys # Try to decrypt each EDK until one succeeds # TODO: probably just enforce |EDKs| == 1 and remove loop @@ -75,8 +76,8 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): try: edk_bytes = edk.encrypted_data_key if edk.key_provider_info == "kms+context": - encryption_context_from_request = decMaterials.get('encryption_context_from_request', {}) - encryption_context_stored = decMaterials.get('encryption_context_stored', {}) + encryption_context_from_request = decMaterials.encryption_context_from_request + encryption_context_stored = decMaterials.encryption_context_stored # Default EC MUST NOT be passed in via request if KMS_CONTEXT_DEFAULT_KEY in encryption_context_from_request: @@ -100,9 +101,9 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): response = self.kms_client.decrypt( KeyId = self.kms_key_id, CiphertextBlob = edk_bytes, - EncryptionContext = decMaterials['encryption_context_stored'] + EncryptionContext = decMaterials.encryption_context_stored ) - decMaterials['PDK'] = response['Plaintext'] + decMaterials.plaintext_data_key = response['Plaintext'] return decMaterials except Exception as e: last_exception = e diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py index 32b0b880..f532dfb5 100644 --- a/src/s3_encryption/materials/materials.py +++ b/src/s3_encryption/materials/materials.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from attrs import define, field -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List from .encrypted_data_key import EncryptedDataKey @define @@ -57,3 +57,70 @@ def to_dict(self) -> Dict[str, Any]: result['PDK'] = self.plaintext_data_key return result + + +@define +class DecryptionMaterials: + """ + Class representing decryption materials for S3 encryption. + + This class provides a structured way to handle decryption materials + with fields corresponding to the data needed for decryption operations. + + Attributes: + iv (Optional[bytes]): The initialization vector used for content encryption + encrypted_data_keys (List[EncryptedDataKey]): List of encrypted data keys to try + encryption_context_stored (Dict[str, str]): Encryption context stored with the object + encryption_context_from_request (Dict[str, str]): Encryption context provided in the request + plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) + """ + iv: Optional[bytes] = field(default=None) + encrypted_data_keys: List[EncryptedDataKey] = field(factory=list) + encryption_context_stored: Dict[str, str] = field(factory=dict) + encryption_context_from_request: Dict[str, str] = field(factory=dict) + plaintext_data_key: Optional[bytes] = field(default=None) + + @classmethod + def from_dict(cls, materials_dict: Dict[str, Any]) -> 'DecryptionMaterials': + """ + Create a DecryptionMaterials instance from a dictionary. + + Args: + materials_dict (Dict[str, Any]): Dictionary containing decryption materials + + Returns: + DecryptionMaterials: A new instance with fields populated from the dictionary + """ + return cls( + iv=materials_dict.get('iv'), + encrypted_data_keys=materials_dict.get('encrypted_data_keys', []), + encryption_context_stored=materials_dict.get('encryption_context_stored', {}), + encryption_context_from_request=materials_dict.get('encryption_context_from_request', {}), + plaintext_data_key=materials_dict.get('PDK') + ) + + def to_dict(self) -> Dict[str, Any]: + """ + Convert the DecryptionMaterials instance to a dictionary. + + Returns: + Dict[str, Any]: Dictionary containing decryption materials + """ + result = {} + + if self.iv is not None: + result['iv'] = self.iv + + if self.encrypted_data_keys: + result['encrypted_data_keys'] = self.encrypted_data_keys + + if self.encryption_context_stored: + result['encryption_context_stored'] = self.encryption_context_stored + + if self.encryption_context_from_request: + result['encryption_context_from_request'] = self.encryption_context_from_request + + if self.plaintext_data_key is not None: + result['PDK'] = self.plaintext_data_key + + return result diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index 7ddee40c..ae164be8 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -5,9 +5,10 @@ import base64 from .materials.crypto_materials_manager import AbstractCryptoMaterialsManager from .materials.encrypted_data_key import EncryptedDataKey -from .materials.materials import EncryptionMaterials +from .materials.materials import EncryptionMaterials, DecryptionMaterials from cryptography.hazmat.primitives.ciphers.aead import AESGCM from .metadata import ObjectMetadata +from typing import Dict, Any, Optional, List, Union @define class PutEncryptedObjectPipeline: @@ -92,11 +93,11 @@ def decrypt(self, response, encryption_context={}): Decrypt the data after it is retrieved from S3. Args: - encrypted_data (bytes): The encrypted data retrieved from S3 - encryption_metadata (dict, optional): Metadata about the encryption + response (dict): The response from S3 containing the encrypted data and metadata + encryption_context (dict, optional): Additional context for decryption Returns: - bytes or str: The decrypted data + bytes: The decrypted data """ # Convert the metadata dictionary to an ObjectMetadata instance encrypted_data = response.get('Body').read() @@ -131,15 +132,18 @@ def decrypt(self, response, encryption_context={}): ) encrypted_data_keys.append(legacy_encrypted_data_key) - dec_mat_req = { - "iv": iv_bytes, - "encrypted_data_keys": encrypted_data_keys, - "encryption_context_stored": metadata.encrypted_data_key_context, - "encryption_context_from_request": encryption_context - } - dec_mats = self.cmm.decryptMaterials(dec_mat_req) + # Create a DecryptionMaterials instance + dec_materials = DecryptionMaterials( + iv=iv_bytes, + encrypted_data_keys=encrypted_data_keys, + encryption_context_stored=metadata.encrypted_data_key_context or {}, + encryption_context_from_request=encryption_context + ) + + # Get decryption materials from the crypto materials manager + dec_materials = self.cmm.decryptMaterials(dec_materials) - aesgcm = AESGCM(dec_mats['PDK']) + aesgcm = AESGCM(dec_materials.plaintext_data_key) plaintext = aesgcm.decrypt( nonce=iv_bytes, diff --git a/test/test_decryption_materials.py b/test/test_decryption_materials.py new file mode 100644 index 00000000..d6c08530 --- /dev/null +++ b/test/test_decryption_materials.py @@ -0,0 +1,89 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from src.s3_encryption.materials.materials import DecryptionMaterials +from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey + +class TestDecryptionMaterials(unittest.TestCase): + def test_create_decryption_materials(self): + """Test creating a DecryptionMaterials instance.""" + materials = DecryptionMaterials() + self.assertEqual(materials.encrypted_data_keys, []) + self.assertEqual(materials.encryption_context_stored, {}) + self.assertEqual(materials.encryption_context_from_request, {}) + self.assertIsNone(materials.iv) + self.assertIsNone(materials.plaintext_data_key) + + def test_create_with_parameters(self): + """Test creating a DecryptionMaterials instance with parameters.""" + iv = b'initialization-vector' + encrypted_data_keys = [ + EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + ] + encryption_context_stored = {"key1": "value1"} + encryption_context_from_request = {"key2": "value2"} + plaintext_data_key = b'plaintext-data-key' + + materials = DecryptionMaterials( + iv=iv, + encrypted_data_keys=encrypted_data_keys, + encryption_context_stored=encryption_context_stored, + encryption_context_from_request=encryption_context_from_request, + plaintext_data_key=plaintext_data_key + ) + + self.assertEqual(materials.iv, iv) + self.assertEqual(materials.encrypted_data_keys, encrypted_data_keys) + self.assertEqual(materials.encryption_context_stored, encryption_context_stored) + self.assertEqual(materials.encryption_context_from_request, encryption_context_from_request) + self.assertEqual(materials.plaintext_data_key, plaintext_data_key) + + def test_from_dict(self): + """Test creating a DecryptionMaterials instance from a dictionary.""" + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + materials_dict = { + 'iv': b'initialization-vector', + 'encrypted_data_keys': [edk], + 'encryption_context_stored': {"key1": "value1"}, + 'encryption_context_from_request': {"key2": "value2"}, + 'PDK': b'plaintext-data-key' + } + materials = DecryptionMaterials.from_dict(materials_dict) + self.assertEqual(materials.iv, b'initialization-vector') + self.assertEqual(materials.encrypted_data_keys, [edk]) + self.assertEqual(materials.encryption_context_stored, {"key1": "value1"}) + self.assertEqual(materials.encryption_context_from_request, {"key2": "value2"}) + self.assertEqual(materials.plaintext_data_key, b'plaintext-data-key') + + def test_to_dict(self): + """Test converting a DecryptionMaterials instance to a dictionary.""" + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + materials = DecryptionMaterials( + iv=b'initialization-vector', + encrypted_data_keys=[edk], + encryption_context_stored={"key1": "value1"}, + encryption_context_from_request={"key2": "value2"}, + plaintext_data_key=b'plaintext-data-key' + ) + materials_dict = materials.to_dict() + self.assertEqual(materials_dict['iv'], b'initialization-vector') + self.assertEqual(materials_dict['encrypted_data_keys'], [edk]) + self.assertEqual(materials_dict['encryption_context_stored'], {"key1": "value1"}) + self.assertEqual(materials_dict['encryption_context_from_request'], {"key2": "value2"}) + self.assertEqual(materials_dict['PDK'], b'plaintext-data-key') + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py new file mode 100644 index 00000000..5086715a --- /dev/null +++ b/test/test_decryption_materials_integration.py @@ -0,0 +1,118 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from unittest.mock import MagicMock, patch +from src.s3_encryption.materials.materials import DecryptionMaterials +from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey +from src.s3_encryption.materials.keyring import S3Keyring +from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager + +class TestDecryptionMaterialsIntegration(unittest.TestCase): + def test_keyring_onDecrypt(self): + """Test that S3Keyring.onDecrypt properly handles DecryptionMaterials.""" + # Create a keyring + keyring = S3Keyring() + + # Create an encrypted data key + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + + # Create decryption materials + materials = DecryptionMaterials( + iv=b'initialization-vector', + encrypted_data_keys=[edk], + encryption_context_stored={"key1": "value1"}, + encryption_context_from_request={"key2": "value2"} + ) + + # Mock the validation method to return the materials + with patch.object(S3Keyring, 'onDecrypt', return_value=materials) as mock_onDecrypt: + # Call onDecrypt + result = keyring.onDecrypt(materials, [edk]) + + # Verify the result is a DecryptionMaterials instance + self.assertIsInstance(result, DecryptionMaterials) + self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.encrypted_data_keys, [edk]) + self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) + self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) + + def test_cmm_decryptMaterials_with_dict(self): + """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles dictionary input.""" + # Create a mock keyring + keyring = MagicMock() + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + keyring.onDecrypt.return_value = DecryptionMaterials( + iv=b'initialization-vector', + encrypted_data_keys=[edk], + encryption_context_stored={"key1": "value1"}, + encryption_context_from_request={"key2": "value2"}, + plaintext_data_key=b'plaintext-data-key' + ) + + # Create a CMM + cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + # Call decryptMaterials with a dictionary + result = cmm.decryptMaterials({ + 'iv': b'initialization-vector', + 'encrypted_data_keys': [edk], + 'encryption_context_stored': {"key1": "value1"}, + 'encryption_context_from_request': {"key2": "value2"} + }) + + # Verify the result is a DecryptionMaterials instance + self.assertIsInstance(result, DecryptionMaterials) + self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.encrypted_data_keys, [edk]) + self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) + self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) + self.assertEqual(result.plaintext_data_key, b'plaintext-data-key') + + def test_cmm_decryptMaterials_with_materials(self): + """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles DecryptionMaterials input.""" + # Create a mock keyring + keyring = MagicMock() + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + keyring.onDecrypt.return_value = DecryptionMaterials( + iv=b'initialization-vector', + encrypted_data_keys=[edk], + encryption_context_stored={"key1": "value1"}, + encryption_context_from_request={"key2": "value2"}, + plaintext_data_key=b'plaintext-data-key' + ) + + # Create a CMM + cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + # Call decryptMaterials with a DecryptionMaterials instance + materials = DecryptionMaterials( + iv=b'initialization-vector', + encrypted_data_keys=[edk], + encryption_context_stored={"key1": "value1"}, + encryption_context_from_request={"key2": "value2"} + ) + result = cmm.decryptMaterials(materials) + + # Verify the result is a DecryptionMaterials instance + self.assertIsInstance(result, DecryptionMaterials) + self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.encrypted_data_keys, [edk]) + self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) + self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) + self.assertEqual(result.plaintext_data_key, b'plaintext-data-key') + +if __name__ == '__main__': + unittest.main() From d4baef14ce81be3a07f37472854b989d9dad677f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 15:25:55 -0700 Subject: [PATCH 04/36] add Github workflows --- .github/workflows/main.yml | 21 ++++++++++++++++ .github/workflows/test.yml | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..b7396de4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: Main Workflow + +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: + inputs: + python-version: + description: 'Python version to use' + default: '3.11' + required: false + type: string + +jobs: + run-tests: + name: Run Tests + uses: ./.github/workflows/test.yml + with: + python-version: ${{ inputs.python-version || '3.11' }} + secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..4b5db913 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Run Tests + +on: + workflow_call: + # Optional inputs that can be provided when calling this workflow + inputs: + python-version: + description: 'Python version to use' + default: '3.11' + required: false + type: string + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version || '3.11' }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.7.1 + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install dependencies + run: poetry install + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role + aws-region: us-west-2 + + - name: Run unit tests + run: poetry run pytest test/ --verbose + + - name: Run integration tests + run: poetry run pytest test/integration/ --verbose + env: + CI_S3_BUCKET: ${{ secrets.CI_S3_BUCKET }} + CI_KMS_KEY_ALIAS: ${{ secrets.CI_KMS_KEY_ALIAS }} From fae581968fce072ab63e4e9549a770569d35dab5 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 15:35:30 -0700 Subject: [PATCH 05/36] permissions --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b5db913..8cb91e38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,9 @@ on: jobs: test: runs-on: ubuntu-latest + permissions: + id-token: write + contents: read steps: - name: Checkout code From 99c9591f241cf4fd3b83f042472693218e9b557f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 15:42:19 -0700 Subject: [PATCH 06/36] fix test --- test/integration/test_i_s3_encryption.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index 981fbd3c..db96aa06 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -1,11 +1,13 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import boto3 +import os from datetime import datetime from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig from s3_encryption.materials.kms_keyring import KmsKeyring -bucket = "s3-ec-python-v3-test" +bucket = os.environ.get("CI_S3_BUCKET", "s3ec-python-github-test-bucket") +kms_key_id = os.environ.get("CI_KMS_KEY_ALIAS", "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key") def test_simple_roundtrip(): key = "simple-rt" @@ -13,7 +15,6 @@ def test_simple_roundtrip(): data = "test input for simple v3 round trip" - kms_key_id = "arn:aws:kms:us-east-2:657301468084:key/1f469b1a-5cfa-4879-9bdf-27b3abd9b8d5" kms_client = boto3.client("kms", region_name="us-east-2") keyring = KmsKeyring(kms_client, kms_key_id) @@ -38,4 +39,4 @@ def test_simple_roundtrip(): print("Output:") print(output) else: - print("Success!") \ No newline at end of file + print("Success!") From 52dc9ab7dd7986c75abf971f63494708fd0d5cd7 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 15:54:11 -0700 Subject: [PATCH 07/36] fix region --- test/integration/test_i_s3_encryption.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index db96aa06..b70e9c09 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -7,6 +7,7 @@ from s3_encryption.materials.kms_keyring import KmsKeyring bucket = os.environ.get("CI_S3_BUCKET", "s3ec-python-github-test-bucket") +region = os.environ.get("CI_AWS_REGION", "us-west-2") kms_key_id = os.environ.get("CI_KMS_KEY_ALIAS", "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key") def test_simple_roundtrip(): @@ -15,7 +16,7 @@ def test_simple_roundtrip(): data = "test input for simple v3 round trip" - kms_client = boto3.client("kms", region_name="us-east-2") + kms_client = boto3.client("kms", region_name=region) keyring = KmsKeyring(kms_client, kms_key_id) From 61096c67f1cb53c70e39a3a7f8946d56ae12b384 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 16:02:39 -0700 Subject: [PATCH 08/36] debug --- src/s3_encryption/materials/keyring.py | 3 ++ test/test_decryption_materials_integration.py | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 222eec7b..e4c6d338 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -96,6 +96,9 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): # Ensure encryption contexts are dictionaries if not isinstance(decMaterials.encryption_context_from_request, dict): + print("EC from req: ") + print(decMaterials.encryption_context_from_request) + print("now raising..") raise S3EncryptionClientError("Encryption context from request must be a dictionary") if not isinstance(decMaterials.encryption_context_stored, dict): diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py index 5086715a..dbbaf431 100644 --- a/test/test_decryption_materials_integration.py +++ b/test/test_decryption_materials_integration.py @@ -41,6 +41,38 @@ def test_keyring_onDecrypt(self): self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) + def test_keyring_onDecrypt_default_EC(self): + """Test that S3Keyring.onDecrypt properly handles DecryptionMaterials.""" + # Create a keyring + keyring = S3Keyring() + + # Create an encrypted data key + edk = EncryptedDataKey( + key_provider_id=b'S3Keyring', + key_provider_info="kms+context", + encrypted_data_key=b'encrypted-data-key' + ) + + # Create decryption materials + materials = DecryptionMaterials( + iv=b'initialization-vector', + encrypted_data_keys=[edk], + encryption_context_stored={}, + encryption_context_from_request={} + ) + + # Mock the validation method to return the materials + with patch.object(S3Keyring, 'onDecrypt', return_value=materials) as mock_onDecrypt: + # Call onDecrypt + result = keyring.onDecrypt(materials, [edk]) + + # Verify the result is a DecryptionMaterials instance + self.assertIsInstance(result, DecryptionMaterials) + self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.encrypted_data_keys, [edk]) + self.assertEqual(result.encryption_context_stored, {}) + self.assertEqual(result.encryption_context_from_request, {}) + def test_cmm_decryptMaterials_with_dict(self): """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles dictionary input.""" # Create a mock keyring From 23362e3d9434d30e74de3bf943fdfce754b567d4 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 16:10:12 -0700 Subject: [PATCH 09/36] or dict --- src/s3_encryption/pipelines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index ae164be8..41ec53dc 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -137,7 +137,7 @@ def decrypt(self, response, encryption_context={}): iv=iv_bytes, encrypted_data_keys=encrypted_data_keys, encryption_context_stored=metadata.encrypted_data_key_context or {}, - encryption_context_from_request=encryption_context + encryption_context_from_request=encryption_context or {} ) # Get decryption materials from the crypto materials manager From 8c76a5bb52167a85f74d9df7fbfe92f47f0aab38 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 16:24:06 -0700 Subject: [PATCH 10/36] debug --- test/integration/test_i_s3_encryption.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index b70e9c09..29b96301 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -18,6 +18,7 @@ def test_simple_roundtrip(): kms_client = boto3.client("kms", region_name=region) + print("KMS Key: " + kms_key_id) keyring = KmsKeyring(kms_client, kms_key_id) wrapped_client = boto3.client("s3") From 24bcb4899a259a4acaf2c50f5ed64c8820947696 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 16:27:15 -0700 Subject: [PATCH 11/36] github env vars --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b7396de4..102b8a74 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,3 +19,6 @@ jobs: with: python-version: ${{ inputs.python-version || '3.11' }} secrets: inherit + env: + CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} + CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} From ef416de7458291fe5bfefd6131464902827c50ff Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 16:29:33 -0700 Subject: [PATCH 12/36] vars not secrets --- .github/workflows/main.yml | 3 --- .github/workflows/test.yml | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 102b8a74..b7396de4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,3 @@ jobs: with: python-version: ${{ inputs.python-version || '3.11' }} secrets: inherit - env: - CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} - CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cb91e38..c60d4a20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,5 +48,5 @@ jobs: - name: Run integration tests run: poetry run pytest test/integration/ --verbose env: - CI_S3_BUCKET: ${{ secrets.CI_S3_BUCKET }} - CI_KMS_KEY_ALIAS: ${{ secrets.CI_KMS_KEY_ALIAS }} + CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} + CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} From 480a3987080e3b8c0e9dcfa61e1098134c6bb388 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 16:31:42 -0700 Subject: [PATCH 13/36] remove debug, raise error on mismatch --- src/s3_encryption/materials/keyring.py | 3 --- test/integration/test_i_s3_encryption.py | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index e4c6d338..222eec7b 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -96,9 +96,6 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): # Ensure encryption contexts are dictionaries if not isinstance(decMaterials.encryption_context_from_request, dict): - print("EC from req: ") - print(decMaterials.encryption_context_from_request) - print("now raising..") raise S3EncryptionClientError("Encryption context from request must be a dictionary") if not isinstance(decMaterials.encryption_context_stored, dict): diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index 29b96301..55b1d678 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -18,27 +18,24 @@ def test_simple_roundtrip(): kms_client = boto3.client("kms", region_name=region) - print("KMS Key: " + kms_key_id) keyring = KmsKeyring(kms_client, kms_key_id) wrapped_client = boto3.client("s3") config = S3EncryptionClientConfig(keyring) s3ec = S3EncryptionClient(wrapped_client, config) s3ec.put_object(Bucket=bucket, Key=key, Data=data) - print("put object success!") get_req = { 'Bucket': bucket, 'Key': key } response = s3ec.get_object(**get_req) output = response['Body'].read().decode('utf-8') - print("get succeeded!") - print(response) if output != data: print("Uh oh! Input and output don't match!") print("Input:") print(input) print("Output:") print(output) + raise RuntimeError else: print("Success!") From 21b59063e7e837e9fd7f423e34b110fbaebe595f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Wed, 6 Aug 2025 17:16:31 -0700 Subject: [PATCH 14/36] run formatter, add linting --- Makefile | 38 ++++ README.md | 73 +++++++ poetry.lock | 204 +++++++++++++++++- pyproject.toml | 23 ++ src/s3_encryption/__init__.py | 67 +++--- src/s3_encryption/materials/__init__.py | 16 +- .../materials/crypto_materials_manager.py | 36 ++-- .../materials/encrypted_data_key.py | 6 +- src/s3_encryption/materials/keyring.py | 62 +++--- src/s3_encryption/materials/kms_keyring.py | 73 ++++--- src/s3_encryption/materials/materials.py | 93 ++++---- src/s3_encryption/metadata.py | 38 ++-- src/s3_encryption/pipelines.py | 97 ++++----- test/integration/test_i_s3_encryption.py | 18 +- test/test_decryption_materials.py | 63 +++--- test/test_decryption_materials_integration.py | 109 +++++----- test/test_encryption_materials.py | 37 ++-- test/test_encryption_materials_integration.py | 51 +++-- test/test_metadata.py | 34 +-- 19 files changed, 763 insertions(+), 375 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..15dd281a --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +.PHONY: lint format test test-unit test-integration install + +# Default target +all: lint test + +# Install dependencies +install: + poetry install + +# Run linting checks +lint: + poetry run black --check . + poetry run isort --check . + # Allow flake8 to fail for now as we're gradually adopting linting standards + poetry run flake8 src/ test/ || true + +# Format code with Black and isort +format: + poetry run black . + poetry run isort . + +# Run all tests +test: test-unit test-integration + +# Run unit tests +test-unit: + poetry run pytest test/ --verbose + +# Run integration tests +test-integration: + poetry run pytest test/integration/ --verbose + +# Clean up cache files +clean: + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type d -name .pytest_cache -exec rm -rf {} + + find . -type d -name .coverage -exec rm -rf {} + + find . -type f -name "*.pyc" -delete diff --git a/README.md b/README.md index 2104b640..c24d1796 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ # Amazon S3 Encryption Client Python +This library provides an S3 client that supports client-side encryption. + +## Development + +### Prerequisites + +- Python 3.11 or higher +- [Poetry](https://python-poetry.org/) for dependency management + +### Setup + +Install dependencies: + +```bash +make install +``` + +### Testing + +Run all tests: + +```bash +make test +``` + +Run unit tests only: + +```bash +make test-unit +``` + +Run integration tests only: + +```bash +make test-integration +``` + +### Code Quality + +This project uses [Black](https://black.readthedocs.io/) for code formatting, [isort](https://pycqa.github.io/isort/) for import sorting, and [Flake8](https://flake8.pycqa.org/) for linting. + +Check code quality: + +```bash +make lint +``` + +Format code with Black and isort: + +```bash +make format +``` + +Clean up cache files: + +```bash +make clean +``` + +#### Linting Standards + +The project is configured with Black, isort, and Flake8 to enforce consistent code style and quality. Currently, Flake8 is set to report issues but not fail the build, allowing for gradual adoption of linting standards. + +Common Flake8 issues in the codebase include: + +- **Missing docstrings** (D100-D104): Add docstrings to modules, classes, and functions +- **Docstring formatting** (D200, D212, D415): Follow Google docstring style +- **Line length** (E501): Keep lines under 100 characters +- **Unused imports** (F401): Remove unused imports +- **Unused variables** (F841): Remove or use assigned variables +- **Code complexity** (C901): Refactor complex functions + +When contributing to this project, please try to fix linting issues in the files you modify. diff --git a/poetry.lock b/poetry.lock index 1ab72864..a89805ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -96,6 +96,50 @@ files = [ DafnyRuntimePython = "4.9.0" pytz = ">=2023.3.post1,<2025.0.0" +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "boto3" version = "1.39.14" @@ -213,6 +257,20 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -284,6 +342,37 @@ files = [ {file = "dafnyruntimepython-4.9.0.tar.gz", hash = "sha256:03a4c2dbbe45c13dc2c7dbefad01812367b3bb217a14b4b848d7e94ef5c08cee"}, ] +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + [[package]] name = "iniconfig" version = "2.1.0" @@ -295,6 +384,20 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + [[package]] name = "jmespath" version = "1.0.1" @@ -306,6 +409,28 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + [[package]] name = "packaging" version = "25.0" @@ -317,6 +442,33 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + [[package]] name = "pluggy" version = "1.6.0" @@ -332,6 +484,17 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + [[package]] name = "pycparser" version = "2.22" @@ -343,6 +506,34 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + [[package]] name = "pygments" version = "2.19.2" @@ -431,6 +622,17 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +files = [ + {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, + {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, +] + [[package]] name = "urllib3" version = "2.5.0" @@ -451,4 +653,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b1781f15e07cc26d093bb8ed243f85d126ac76a46954cc1d1ff29261f1db380c" +content-hash = "e0d80bd0119ad8c72dfd80afa69019310ce4c75a9f81e6da9bb80666657840e2" diff --git a/pyproject.toml b/pyproject.toml index fba01461..89a86e17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,32 @@ boto3 = "^1.37.2" cryptography = "^43.0.1" aws-cryptographic-material-providers = "^1.7.4" attrs = "^25.1.0" + +[tool.poetry.group.dev.dependencies] pytest = "^8.4.1" +black = "^24.3.0" +flake8 = "^7.0.0" +flake8-docstrings = "^1.7.0" +isort = "^5.13.2" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 100 +target-version = ["py311"] +include = '\.pyi?$' + +[tool.flake8] +max-line-length = 100 +exclude = [".git", "__pycache__", "build", "dist"] +max-complexity = 10 +ignore = ["E203", "W503"] # E203 and W503 conflict with Black +docstring-convention = "google" + +[tool.isort] +profile = "black" +line_length = 100 +known_first_party = ["s3_encryption"] diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index 99493f07..ab0a77fe 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -1,84 +1,85 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import io -from botocore.response import StreamingBody + from attrs import define, field -from .pipelines import PutEncryptedObjectPipeline, GetEncryptedObjectPipeline +from botocore.response import StreamingBody + +from .materials.crypto_materials_manager import ( + AbstractCryptoMaterialsManager, + DefaultCryptoMaterialsManager, +) from .materials.keyring import AbstractKeyring -from .materials.crypto_materials_manager import AbstractCryptoMaterialsManager, DefaultCryptoMaterialsManager from .metadata import ObjectMetadata +from .pipelines import GetEncryptedObjectPipeline, PutEncryptedObjectPipeline @define -class S3EncryptionClientConfig(): +class S3EncryptionClientConfig: """ Configuration object for the S3 Encryption Client """ + keyring: AbstractKeyring cmm: AbstractCryptoMaterialsManager = field() + @cmm.default def _default_cmm_for_keyring(self): return DefaultCryptoMaterialsManager(self.keyring) @define -class S3EncryptionClient(): +class S3EncryptionClient: wrapped_s3_client = field() config: S3EncryptionClientConfig = field() - # TODO: I don't know exactly how boto3 works, - # we maybe instead prefer only using kwargs? - # Do we need to provide specific arg overloads? # TODO: rename Data-> Body to match boto def put_object(self, Bucket, Key, Data, EncryptionContext=None, **kwargs): # Create a pipeline for this operation pipeline = PutEncryptedObjectPipeline(self.config.cmm) - # Encrypt the data using the pipeline data_bytes = Data # We probably just shouldn't support strings, use utf8 for now + # TODO: look deeper into this, what does normal boto3 do? if type(Data) == str: - data_bytes = Data.encode('utf-8') - encrypted_data, encryption_metadata = pipeline.encrypt(data_bytes, encryption_context=EncryptionContext) - + data_bytes = Data.encode("utf-8") + encrypted_data, encryption_metadata = pipeline.encrypt( + data_bytes, encryption_context=EncryptionContext + ) + # Add encryption metadata to the request parameters - params = { - 'Bucket': Bucket, - 'Key': Key, - 'Body': encrypted_data, - **kwargs - } - + params = {"Bucket": Bucket, "Key": Key, "Body": encrypted_data, **kwargs} + # Add encryption metadata to the parameters if encryption_metadata: # Merge any existing metadata with our encryption metadata - metadata = params.get('Metadata', {}) + metadata = params.get("Metadata", {}) metadata.update(encryption_metadata) - params['Metadata'] = metadata - + params["Metadata"] = metadata + return self.wrapped_s3_client.put_object(**params) def get_object(self, EncryptionContext=None, **kwargs): # try just straight kwargs - params = { - **kwargs - } - + params = {**kwargs} + # Get the encrypted object from S3 response = self.wrapped_s3_client.get_object(**params) - + # Create a pipeline for this operation pipeline = GetEncryptedObjectPipeline(self.config.cmm) - + # Decrypt the data using the pipeline - decrypted_data = pipeline.decrypt(response, EncryptionContext) #encrypted_data, encryption_metadata) - + decrypted_data = pipeline.decrypt( + response, EncryptionContext + ) # encrypted_data, encryption_metadata) + # Create a new streaming body with the decrypted data stream = io.BytesIO(decrypted_data) streaming_body = StreamingBody(stream, len(decrypted_data)) - + # Update the response with the decrypted data - response['Body'] = streaming_body - + response["Body"] = streaming_body + return response diff --git a/src/s3_encryption/materials/__init__.py b/src/s3_encryption/materials/__init__.py index 79178052..b602bc91 100644 --- a/src/s3_encryption/materials/__init__.py +++ b/src/s3_encryption/materials/__init__.py @@ -1,17 +1,17 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from .keyring import AbstractKeyring -from .kms_keyring import KmsKeyring from .crypto_materials_manager import AbstractCryptoMaterialsManager, DefaultCryptoMaterialsManager from .encrypted_data_key import EncryptedDataKey +from .keyring import AbstractKeyring +from .kms_keyring import KmsKeyring from .materials import EncryptionMaterials __all__ = [ - 'AbstractKeyring', - 'KmsKeyring', - 'AbstractCryptoMaterialsManager', - 'DefaultCryptoMaterialsManager', - 'EncryptedDataKey', - 'EncryptionMaterials' + "AbstractKeyring", + "KmsKeyring", + "AbstractCryptoMaterialsManager", + "DefaultCryptoMaterialsManager", + "EncryptedDataKey", + "EncryptionMaterials", ] diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index f30f3491..c2d67edf 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -1,36 +1,40 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from typing import Any, Dict, List, Union + from attrs import define + from .keyring import AbstractKeyring -from .materials import EncryptionMaterials, DecryptionMaterials -from typing import List, Dict, Any, Union +from .materials import DecryptionMaterials, EncryptionMaterials + # API Stub for CMM -class AbstractCryptoMaterialsManager(): +class AbstractCryptoMaterialsManager: def getEncryptionMaterials(self, encMatsRequest): """ Get encryption materials from the keyring. - + Args: encMatsRequest (Dict[str, Any] or EncryptionMaterials): Request containing encryption parameters - + Returns: EncryptionMaterials: The encryption materials """ raise NotImplementedError - + def decryptMaterials(self, decMatsRequest): """ Decrypt materials using the keyring. - + Args: decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters - + Returns: DecryptionMaterials: The decryption materials """ raise NotImplementedError + @define class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): keyring: AbstractKeyring @@ -38,30 +42,30 @@ class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): def getEncryptionMaterials(self, encMatsRequest): """ Get encryption materials from the keyring. - + Args: encMatsRequest (Dict[str, Any]): Request containing encryption parameters - + Returns: EncryptionMaterials: The encryption materials """ # Convert dictionary to EncryptionMaterials if needed if isinstance(encMatsRequest, dict): materials = EncryptionMaterials( - encryption_context=encMatsRequest.get('encryption_context', {}) + encryption_context=encMatsRequest.get("encryption_context", {}) ) else: materials = encMatsRequest - + return self.keyring.onEncrypt(materials) - + def decryptMaterials(self, decMatsRequest): """ Decrypt materials using the keyring. - + Args: decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters - + Returns: DecryptionMaterials: The decryption materials """ @@ -70,6 +74,6 @@ def decryptMaterials(self, decMatsRequest): materials = DecryptionMaterials.from_dict(decMatsRequest) else: materials = decMatsRequest - + encrypted_data_keys = materials.encrypted_data_keys return self.keyring.onDecrypt(materials, encrypted_data_keys) diff --git a/src/s3_encryption/materials/encrypted_data_key.py b/src/s3_encryption/materials/encrypted_data_key.py index 72fbeae2..0dbfa08f 100644 --- a/src/s3_encryption/materials/encrypted_data_key.py +++ b/src/s3_encryption/materials/encrypted_data_key.py @@ -2,19 +2,21 @@ # SPDX-License-Identifier: Apache-2.0 from attrs import define, field + @define class EncryptedDataKey: """ Class representing an encrypted data key. - + An encrypted data key contains information about the key provider and the encrypted data key itself. - + Attributes: key_provider_info (str): Information about the key provider key_provider_id (bytes): Identifier for the key provider encrypted_data_key (bytes): The encrypted data key """ + key_provider_info: str = field() key_provider_id: bytes = field() encrypted_data_key: bytes = field() diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 222eec7b..98e4f6ab 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -1,26 +1,29 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from typing import List, Optional + from attrs import define, field + from ..exceptions import S3EncryptionClientError -from .materials import EncryptionMaterials, DecryptionMaterials -from typing import List, Optional +from .materials import DecryptionMaterials, EncryptionMaterials + @define -class AbstractKeyring(): +class AbstractKeyring: # Ideally, all keyrings would inherit this field. - # However, attrs doesn't allow us to set a default here, + # However, attrs doesn't allow us to set a default here, # when inheriting keyrings have optional fields. # Even without a default it doesn't seem to play nice with attrs. - #enableLegacyWrappingAlgorithms: bool = field(default=False) + # enableLegacyWrappingAlgorithms: bool = field(default=False) def onEncrypt(self, encMaterials): """ Process encryption materials. - + Args: encMaterials (EncryptionMaterials): Encryption materials to process - + Returns: EncryptionMaterials: The processed encryption materials """ @@ -29,11 +32,11 @@ def onEncrypt(self, encMaterials): def onDecrypt(self, decMaterials, encrypted_data_keys=None): """ Decrypt one of the encrypted data keys and update decMaterials. - + Args: decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. - + Returns: DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) """ @@ -45,60 +48,69 @@ class S3Keyring(AbstractKeyring): """ Base class for S3 encryption keyrings that provides common validation logic. """ - # Ideally this would be set, but attrs doesn't play nice + + # Ideally this would be set, but attrs doesn't play nice # enable_legacy_wrapping_algorithms: bool = field(default=False) def onEncrypt(self, encMaterials): """ Validate encryption materials before encryption. - + Args: encMaterials (EncryptionMaterials or dict): Encryption materials - + Returns: EncryptionMaterials: The validated encryption materials """ # Convert dict to EncryptionMaterials if needed if isinstance(encMaterials, dict): encMaterials = EncryptionMaterials.from_dict(encMaterials) - + # Validate encryption materials if not isinstance(encMaterials, EncryptionMaterials): - raise S3EncryptionClientError("Encryption materials must be an EncryptionMaterials instance or a dictionary") - + raise S3EncryptionClientError( + "Encryption materials must be an EncryptionMaterials instance or a dictionary" + ) + # Ensure encryption_context is a dictionary if not isinstance(encMaterials.encryption_context, dict): raise S3EncryptionClientError("Encryption context must be a dictionary") - + return encMaterials def onDecrypt(self, decMaterials, encrypted_data_keys=None): """ Validate decryption materials before decryption. - + Args: decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. - + Returns: DecryptionMaterials: The validated decryption materials """ # Validate decryption materials if not isinstance(decMaterials, DecryptionMaterials): - raise S3EncryptionClientError("Decryption materials must be a DecryptionMaterials instance") - + raise S3EncryptionClientError( + "Decryption materials must be a DecryptionMaterials instance" + ) + # Use encrypted_data_keys from parameters if provided, otherwise use from decMaterials - edks = encrypted_data_keys if encrypted_data_keys is not None else decMaterials.encrypted_data_keys - + edks = ( + encrypted_data_keys + if encrypted_data_keys is not None + else decMaterials.encrypted_data_keys + ) + # Validate encrypted_data_keys if edks is None or len(edks) == 0: raise S3EncryptionClientError("No encrypted data keys provided") - + # Ensure encryption contexts are dictionaries if not isinstance(decMaterials.encryption_context_from_request, dict): raise S3EncryptionClientError("Encryption context from request must be a dictionary") - + if not isinstance(decMaterials.encryption_context_stored, dict): raise S3EncryptionClientError("Stored encryption context must be a dictionary") - + return decMaterials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 75d75a27..4ba5e31b 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -1,15 +1,18 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from .keyring import S3Keyring -from .encrypted_data_key import EncryptedDataKey -from .materials import EncryptionMaterials, DecryptionMaterials -from ..exceptions import S3EncryptionClientError -from attrs import define, field from typing import List, Optional +from attrs import define, field + +from ..exceptions import S3EncryptionClientError +from .encrypted_data_key import EncryptedDataKey +from .keyring import S3Keyring +from .materials import DecryptionMaterials, EncryptionMaterials + KMS_CONTEXT_DEFAULT_KEY = "aws:x-amz-cek-alg" KMS_V1_DEFAULT_KEY = "kms_cmk_id" + @define class KmsKeyring(S3Keyring): kms_client = field() @@ -19,34 +22,32 @@ class KmsKeyring(S3Keyring): def onEncrypt(self, encMaterials): """ Process encryption materials using KMS. - + Args: encMaterials (EncryptionMaterials): Encryption materials to process - + Returns: EncryptionMaterials: The processed encryption materials with KMS-generated keys """ try: # Call parent class validation encMaterials = super().onEncrypt(encMaterials) - + # Add default encryption context encryption_context = encMaterials.encryption_context encryption_context["aws:x-amz-cek-alg"] = "AES/GCM/NoPadding" response = self.kms_client.generate_data_key( - KeyId = self.kms_key_id, - KeySpec = 'AES_256', - EncryptionContext = encryption_context + KeyId=self.kms_key_id, KeySpec="AES_256", EncryptionContext=encryption_context ) # Create an EncryptedDataKey instance encrypted_data_key = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=response['CiphertextBlob'] + encrypted_data_key=response["CiphertextBlob"], ) encMaterials.encrypted_data_key = encrypted_data_key - encMaterials.plaintext_data_key = response['Plaintext'] + encMaterials.plaintext_data_key = response["Plaintext"] return encMaterials except Exception as e: raise @@ -54,21 +55,25 @@ def onEncrypt(self, encMaterials): def onDecrypt(self, decMaterials, encrypted_data_keys=None): """ Decrypt one of the encrypted data keys and update decMaterials. - + Args: decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. - + Returns: DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) """ try: # Call parent class validation decMaterials = super().onDecrypt(decMaterials, encrypted_data_keys) - + # Use encrypted_data_keys from parameters if provided, otherwise use from decMaterials - edks = encrypted_data_keys if encrypted_data_keys is not None else decMaterials.encrypted_data_keys - + edks = ( + encrypted_data_keys + if encrypted_data_keys is not None + else decMaterials.encrypted_data_keys + ) + # Try to decrypt each EDK until one succeeds # TODO: probably just enforce |EDKs| == 1 and remove loop last_exception = None @@ -76,12 +81,16 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): try: edk_bytes = edk.encrypted_data_key if edk.key_provider_info == "kms+context": - encryption_context_from_request = decMaterials.encryption_context_from_request + encryption_context_from_request = ( + decMaterials.encryption_context_from_request + ) encryption_context_stored = decMaterials.encryption_context_stored # Default EC MUST NOT be passed in via request if KMS_CONTEXT_DEFAULT_KEY in encryption_context_from_request: - raise S3EncryptionClientError(f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the S3 encryption client") + raise S3EncryptionClientError( + f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the S3 encryption client" + ) # The stored EC, minus default key/values, MUST match provided EC encryption_context_stored_copy = encryption_context_stored.copy() @@ -89,26 +98,32 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): encryption_context_stored_copy.pop(KMS_CONTEXT_DEFAULT_KEY, None) if encryption_context_stored_copy != encryption_context_from_request: # TODO: modeled error - raise S3EncryptionClientError("Provided encryption context does not match information retrieved from S3") + raise S3EncryptionClientError( + "Provided encryption context does not match information retrieved from S3" + ) # Update decMaterials with the modified encryption context elif edk.key_provider_info == "kms": if not self.enable_legacy_wrapping_algorithms: - raise S3EncryptionClientError(f"Enable legacy wrapping algorithms to use legacy key wrapping algorithm: {edk.key_provider_info}") + raise S3EncryptionClientError( + f"Enable legacy wrapping algorithms to use legacy key wrapping algorithm: {edk.key_provider_info}" + ) else: - raise S3EncryptionClientError(f"{edk.key_provider_info} is not a valid key wrapping algorithm!") + raise S3EncryptionClientError( + f"{edk.key_provider_info} is not a valid key wrapping algorithm!" + ) response = self.kms_client.decrypt( - KeyId = self.kms_key_id, - CiphertextBlob = edk_bytes, - EncryptionContext = decMaterials.encryption_context_stored + KeyId=self.kms_key_id, + CiphertextBlob=edk_bytes, + EncryptionContext=decMaterials.encryption_context_stored, ) - decMaterials.plaintext_data_key = response['Plaintext'] + decMaterials.plaintext_data_key = response["Plaintext"] return decMaterials except Exception as e: last_exception = e continue - + # If we get here, none of the EDKs could be decrypted if last_exception: raise last_exception diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py index f532dfb5..ffab848d 100644 --- a/src/s3_encryption/materials/materials.py +++ b/src/s3_encryption/materials/materials.py @@ -1,61 +1,65 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from typing import Any, Dict, List, Optional + from attrs import define, field -from typing import Optional, Dict, Any, List + from .encrypted_data_key import EncryptedDataKey + @define class EncryptionMaterials: """ Class representing encryption materials for S3 encryption. - + This class provides a structured way to handle encryption materials with fields corresponding to the data needed for encryption operations. - + Attributes: encryption_context (Dict[str, str]): Context information for encryption encrypted_data_key (Optional[EncryptedDataKey]): The encrypted data key plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) """ + encryption_context: Dict[str, str] = field(factory=dict) encrypted_data_key: Optional[EncryptedDataKey] = field(default=None) plaintext_data_key: Optional[bytes] = field(default=None) - + @classmethod - def from_dict(cls, materials_dict: Dict[str, Any]) -> 'EncryptionMaterials': + def from_dict(cls, materials_dict: Dict[str, Any]) -> "EncryptionMaterials": """ Create an EncryptionMaterials instance from a dictionary. - + Args: materials_dict (Dict[str, Any]): Dictionary containing encryption materials - + Returns: EncryptionMaterials: A new instance with fields populated from the dictionary """ return cls( - encryption_context=materials_dict.get('encryption_context', {}), - encrypted_data_key=materials_dict.get('encrypted_data_key'), - plaintext_data_key=materials_dict.get('PDK') + encryption_context=materials_dict.get("encryption_context", {}), + encrypted_data_key=materials_dict.get("encrypted_data_key"), + plaintext_data_key=materials_dict.get("PDK"), ) - + def to_dict(self) -> Dict[str, Any]: """ Convert the EncryptionMaterials instance to a dictionary. - + Returns: Dict[str, Any]: Dictionary containing encryption materials """ result = {} - + if self.encryption_context: - result['encryption_context'] = self.encryption_context - + result["encryption_context"] = self.encryption_context + if self.encrypted_data_key is not None: - result['encrypted_data_key'] = self.encrypted_data_key - + result["encrypted_data_key"] = self.encrypted_data_key + if self.plaintext_data_key is not None: - result['PDK'] = self.plaintext_data_key - + result["PDK"] = self.plaintext_data_key + return result @@ -63,10 +67,10 @@ def to_dict(self) -> Dict[str, Any]: class DecryptionMaterials: """ Class representing decryption materials for S3 encryption. - + This class provides a structured way to handle decryption materials with fields corresponding to the data needed for decryption operations. - + Attributes: iv (Optional[bytes]): The initialization vector used for content encryption encrypted_data_keys (List[EncryptedDataKey]): List of encrypted data keys to try @@ -74,53 +78,56 @@ class DecryptionMaterials: encryption_context_from_request (Dict[str, str]): Encryption context provided in the request plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) """ + iv: Optional[bytes] = field(default=None) encrypted_data_keys: List[EncryptedDataKey] = field(factory=list) encryption_context_stored: Dict[str, str] = field(factory=dict) encryption_context_from_request: Dict[str, str] = field(factory=dict) plaintext_data_key: Optional[bytes] = field(default=None) - + @classmethod - def from_dict(cls, materials_dict: Dict[str, Any]) -> 'DecryptionMaterials': + def from_dict(cls, materials_dict: Dict[str, Any]) -> "DecryptionMaterials": """ Create a DecryptionMaterials instance from a dictionary. - + Args: materials_dict (Dict[str, Any]): Dictionary containing decryption materials - + Returns: DecryptionMaterials: A new instance with fields populated from the dictionary """ return cls( - iv=materials_dict.get('iv'), - encrypted_data_keys=materials_dict.get('encrypted_data_keys', []), - encryption_context_stored=materials_dict.get('encryption_context_stored', {}), - encryption_context_from_request=materials_dict.get('encryption_context_from_request', {}), - plaintext_data_key=materials_dict.get('PDK') + iv=materials_dict.get("iv"), + encrypted_data_keys=materials_dict.get("encrypted_data_keys", []), + encryption_context_stored=materials_dict.get("encryption_context_stored", {}), + encryption_context_from_request=materials_dict.get( + "encryption_context_from_request", {} + ), + plaintext_data_key=materials_dict.get("PDK"), ) - + def to_dict(self) -> Dict[str, Any]: """ Convert the DecryptionMaterials instance to a dictionary. - + Returns: Dict[str, Any]: Dictionary containing decryption materials """ result = {} - + if self.iv is not None: - result['iv'] = self.iv - + result["iv"] = self.iv + if self.encrypted_data_keys: - result['encrypted_data_keys'] = self.encrypted_data_keys - + result["encrypted_data_keys"] = self.encrypted_data_keys + if self.encryption_context_stored: - result['encryption_context_stored'] = self.encryption_context_stored - + result["encryption_context_stored"] = self.encryption_context_stored + if self.encryption_context_from_request: - result['encryption_context_from_request'] = self.encryption_context_from_request - + result["encryption_context_from_request"] = self.encryption_context_from_request + if self.plaintext_data_key is not None: - result['PDK'] = self.plaintext_data_key - + result["PDK"] = self.plaintext_data_key + return result diff --git a/src/s3_encryption/metadata.py b/src/s3_encryption/metadata.py index 4894792a..ce963328 100644 --- a/src/s3_encryption/metadata.py +++ b/src/s3_encryption/metadata.py @@ -1,18 +1,19 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import json +from typing import Any, Dict, Optional + from attrs import define, field -from typing import Optional, Dict, Any @define class ObjectMetadata: """ Class representing metadata for encrypted S3 objects. - + This class provides a structured way to handle encryption metadata with fields corresponding to standard S3 encryption headers. - + All fields are optional and correspond to the following S3 encryption headers: - encrypted_data_key_v1: The encrypted data key (legacy format) - encrypted_data_key_v2: The encrypted data key (current format) @@ -23,6 +24,7 @@ class ObjectMetadata: - content_cipher_tag_length: The length of the authentication tag - instruction_file: Marker for instruction files """ + # The encrypted data key (legacy format) encrypted_data_key_v1: Optional[str] = field(default=None) # The encrypted data key (current format) @@ -51,13 +53,13 @@ class ObjectMetadata: INSTRUCTION_FILE = "x-amz-crypto-instr-file" @classmethod - def from_dict(cls, metadata_dict: Dict[str, Any]) -> 'ObjectMetadata': + def from_dict(cls, metadata_dict: Dict[str, Any]) -> "ObjectMetadata": """ Create an ObjectMetadata instance from a dictionary. - + Args: metadata_dict (Dict[str, Any]): Dictionary containing metadata keys and values - + Returns: ObjectMetadata: A new instance with fields populated from the dictionary """ @@ -67,7 +69,7 @@ def from_dict(cls, metadata_dict: Dict[str, Any]) -> 'ObjectMetadata': context_str = metadata_dict.get(cls.ENCRYPTED_DATA_KEY_CONTEXT) if context_str is not None: encryption_context = json.loads(context_str) - + return cls( encrypted_data_key_v1=metadata_dict.get(cls.ENCRYPTED_DATA_KEY_V1), encrypted_data_key_v2=metadata_dict.get(cls.ENCRYPTED_DATA_KEY_V2), @@ -76,40 +78,40 @@ def from_dict(cls, metadata_dict: Dict[str, Any]) -> 'ObjectMetadata': content_iv=metadata_dict.get(cls.CONTENT_IV), content_cipher=metadata_dict.get(cls.CONTENT_CIPHER), content_cipher_tag_length=metadata_dict.get(cls.CONTENT_CIPHER_TAG_LENGTH), - instruction_file=metadata_dict.get(cls.INSTRUCTION_FILE) + instruction_file=metadata_dict.get(cls.INSTRUCTION_FILE), ) def to_dict(self) -> Dict[str, str]: """ Convert the ObjectMetadata instance to a dictionary. - + Returns: Dict[str, str]: Dictionary containing non-None metadata values """ result = {} - + if self.encrypted_data_key_v1 is not None: result[self.ENCRYPTED_DATA_KEY_V1] = self.encrypted_data_key_v1 - + if self.encrypted_data_key_v2 is not None: result[self.ENCRYPTED_DATA_KEY_V2] = self.encrypted_data_key_v2 - + if self.encrypted_data_key_algorithm is not None: result[self.ENCRYPTED_DATA_KEY_ALGORITHM] = self.encrypted_data_key_algorithm - + if self.encrypted_data_key_context is not None: result[self.ENCRYPTED_DATA_KEY_CONTEXT] = json.dumps(self.encrypted_data_key_context) - + if self.content_iv is not None: result[self.CONTENT_IV] = self.content_iv - + if self.content_cipher is not None: result[self.CONTENT_CIPHER] = self.content_cipher - + if self.content_cipher_tag_length is not None: result[self.CONTENT_CIPHER_TAG_LENGTH] = self.content_cipher_tag_length - + if self.instruction_file is not None: result[self.INSTRUCTION_FILE] = self.instruction_file - + return result diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index 41ec53dc..c207fd45 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -1,33 +1,37 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from attrs import define, field -import os import base64 +import os +from typing import Any, Dict, List, Optional, Union + +from attrs import define, field +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + from .materials.crypto_materials_manager import AbstractCryptoMaterialsManager from .materials.encrypted_data_key import EncryptedDataKey -from .materials.materials import EncryptionMaterials, DecryptionMaterials -from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from .materials.materials import DecryptionMaterials, EncryptionMaterials from .metadata import ObjectMetadata -from typing import Dict, Any, Optional, List, Union + @define class PutEncryptedObjectPipeline: """ Pipeline for encrypting objects before they are put into S3. - + This pipeline handles only the encryption process for S3 objects. The actual S3 API calls are handled by the S3EncryptionClient. """ + cmm: AbstractCryptoMaterialsManager = field() - + def encrypt(self, plaintext, encryption_context=None): """ Encrypt the data before it is stored in S3. - + Args: data (bytes or str): The data to be encrypted encryption_context (dict, optional): Additional context for encryption - + Returns: bytes: The encrypted data dict: Metadata about the encryption to be stored with the object @@ -36,45 +40,41 @@ def encrypt(self, plaintext, encryption_context=None): enc_mats_request = EncryptionMaterials( encryption_context={} if encryption_context is None else encryption_context ) - + # Get encryption materials from the crypto materials manager enc_mats = self.cmm.getEncryptionMaterials(enc_mats_request) - + # Generate initialization vector iv = os.urandom(12) - + # Encrypt the data if enc_mats.plaintext_data_key is None: raise RuntimeError("No plaintext data key found!") - + aesgcm = AESGCM(enc_mats.plaintext_data_key) - ciphertext = aesgcm.encrypt( - nonce=iv, - data=plaintext, - associated_data=None - ) + ciphertext = aesgcm.encrypt(nonce=iv, data=plaintext, associated_data=None) encrypted_data = ciphertext - b64_iv = base64.b64encode(iv).decode('utf-8') - + b64_iv = base64.b64encode(iv).decode("utf-8") + # Get the encrypted data key if enc_mats.encrypted_data_key is None: raise RuntimeError("No encrypted data key found!") - + edk_bytes = enc_mats.encrypted_data_key.encrypted_data_key - b64_edk = base64.b64encode(edk_bytes).decode('utf-8') - + b64_edk = base64.b64encode(edk_bytes).decode("utf-8") + # Create metadata using the ObjectMetadata class metadata = ObjectMetadata( encrypted_data_key_v2=b64_edk, encrypted_data_key_algorithm="kms+context", content_iv=b64_iv, content_cipher="AES/GCM/NoPadding", - encrypted_data_key_context=enc_mats.encryption_context + encrypted_data_key_context=enc_mats.encryption_context, ) - + # Convert to dictionary for storage in S3 metadata encryption_metadata = metadata.to_dict() - + return encrypted_data, encryption_metadata @@ -82,73 +82,70 @@ def encrypt(self, plaintext, encryption_context=None): class GetEncryptedObjectPipeline: """ Pipeline for decrypting objects after they are retrieved from S3. - + This pipeline handles only the decryption process for S3 objects. The actual S3 API calls are handled by the S3EncryptionClient. """ + cmm: AbstractCryptoMaterialsManager = field() - + def decrypt(self, response, encryption_context={}): """ Decrypt the data after it is retrieved from S3. - + Args: response (dict): The response from S3 containing the encrypted data and metadata encryption_context (dict, optional): Additional context for decryption - + Returns: bytes: The decrypted data """ # Convert the metadata dictionary to an ObjectMetadata instance - encrypted_data = response.get('Body').read() - encryption_metadata = response.get('Metadata', {}) + encrypted_data = response.get("Body").read() + encryption_metadata = response.get("Metadata", {}) metadata = ObjectMetadata.from_dict(encryption_metadata) - + iv_b64 = metadata.content_iv edk_b64 = metadata.encrypted_data_key_v2 - + # TODO: probably move this to ObjectMetadata iv_bytes = base64.b64decode(iv_b64) - + # Create a list of encrypted data keys to try encrypted_data_keys = [] # Create an instance of EncryptedDataKey if edk_b64: edk_bytes = base64.b64decode(edk_b64) encrypted_data_key = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info=metadata.encrypted_data_key_algorithm, - encrypted_data_key=edk_bytes + encrypted_data_key=edk_bytes, ) encrypted_data_keys.append(encrypted_data_key) - + # Also check for legacy encrypted data key (v1) if available if metadata.encrypted_data_key_v1: legacy_edk_bytes = base64.b64decode(metadata.encrypted_data_key_v1) legacy_encrypted_data_key = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info=metadata.encrypted_data_key_algorithm, - encrypted_data_key=legacy_edk_bytes + encrypted_data_key=legacy_edk_bytes, ) encrypted_data_keys.append(legacy_encrypted_data_key) - + # Create a DecryptionMaterials instance dec_materials = DecryptionMaterials( iv=iv_bytes, encrypted_data_keys=encrypted_data_keys, encryption_context_stored=metadata.encrypted_data_key_context or {}, - encryption_context_from_request=encryption_context or {} + encryption_context_from_request=encryption_context or {}, ) - + # Get decryption materials from the crypto materials manager dec_materials = self.cmm.decryptMaterials(dec_materials) - + aesgcm = AESGCM(dec_materials.plaintext_data_key) - plaintext = aesgcm.decrypt( - nonce=iv_bytes, - data=encrypted_data, - associated_data=None - ) - + plaintext = aesgcm.decrypt(nonce=iv_bytes, data=encrypted_data, associated_data=None) + return plaintext diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index 55b1d678..d956f321 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -1,14 +1,19 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import boto3 import os from datetime import datetime + +import boto3 + from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig from s3_encryption.materials.kms_keyring import KmsKeyring bucket = os.environ.get("CI_S3_BUCKET", "s3ec-python-github-test-bucket") region = os.environ.get("CI_AWS_REGION", "us-west-2") -kms_key_id = os.environ.get("CI_KMS_KEY_ALIAS", "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key") +kms_key_id = os.environ.get( + "CI_KMS_KEY_ALIAS", "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key" +) + def test_simple_roundtrip(): key = "simple-rt" @@ -24,12 +29,9 @@ def test_simple_roundtrip(): config = S3EncryptionClientConfig(keyring) s3ec = S3EncryptionClient(wrapped_client, config) s3ec.put_object(Bucket=bucket, Key=key, Data=data) - get_req = { - 'Bucket': bucket, - 'Key': key - } + get_req = {"Bucket": bucket, "Key": key} response = s3ec.get_object(**get_req) - output = response['Body'].read().decode('utf-8') + output = response["Body"].read().decode("utf-8") if output != data: print("Uh oh! Input and output don't match!") print("Input:") @@ -37,5 +39,5 @@ def test_simple_roundtrip(): print("Output:") print(output) raise RuntimeError - else: + else: print("Success!") diff --git a/test/test_decryption_materials.py b/test/test_decryption_materials.py index d6c08530..2ada8af8 100644 --- a/test/test_decryption_materials.py +++ b/test/test_decryption_materials.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import unittest -from src.s3_encryption.materials.materials import DecryptionMaterials + from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey +from src.s3_encryption.materials.materials import DecryptionMaterials + class TestDecryptionMaterials(unittest.TestCase): def test_create_decryption_materials(self): @@ -14,76 +16,77 @@ def test_create_decryption_materials(self): self.assertEqual(materials.encryption_context_from_request, {}) self.assertIsNone(materials.iv) self.assertIsNone(materials.plaintext_data_key) - + def test_create_with_parameters(self): """Test creating a DecryptionMaterials instance with parameters.""" - iv = b'initialization-vector' + iv = b"initialization-vector" encrypted_data_keys = [ EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) ] encryption_context_stored = {"key1": "value1"} encryption_context_from_request = {"key2": "value2"} - plaintext_data_key = b'plaintext-data-key' - + plaintext_data_key = b"plaintext-data-key" + materials = DecryptionMaterials( iv=iv, encrypted_data_keys=encrypted_data_keys, encryption_context_stored=encryption_context_stored, encryption_context_from_request=encryption_context_from_request, - plaintext_data_key=plaintext_data_key + plaintext_data_key=plaintext_data_key, ) - + self.assertEqual(materials.iv, iv) self.assertEqual(materials.encrypted_data_keys, encrypted_data_keys) self.assertEqual(materials.encryption_context_stored, encryption_context_stored) self.assertEqual(materials.encryption_context_from_request, encryption_context_from_request) self.assertEqual(materials.plaintext_data_key, plaintext_data_key) - + def test_from_dict(self): """Test creating a DecryptionMaterials instance from a dictionary.""" edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) materials_dict = { - 'iv': b'initialization-vector', - 'encrypted_data_keys': [edk], - 'encryption_context_stored': {"key1": "value1"}, - 'encryption_context_from_request': {"key2": "value2"}, - 'PDK': b'plaintext-data-key' + "iv": b"initialization-vector", + "encrypted_data_keys": [edk], + "encryption_context_stored": {"key1": "value1"}, + "encryption_context_from_request": {"key2": "value2"}, + "PDK": b"plaintext-data-key", } materials = DecryptionMaterials.from_dict(materials_dict) - self.assertEqual(materials.iv, b'initialization-vector') + self.assertEqual(materials.iv, b"initialization-vector") self.assertEqual(materials.encrypted_data_keys, [edk]) self.assertEqual(materials.encryption_context_stored, {"key1": "value1"}) self.assertEqual(materials.encryption_context_from_request, {"key2": "value2"}) - self.assertEqual(materials.plaintext_data_key, b'plaintext-data-key') - + self.assertEqual(materials.plaintext_data_key, b"plaintext-data-key") + def test_to_dict(self): """Test converting a DecryptionMaterials instance to a dictionary.""" edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) materials = DecryptionMaterials( - iv=b'initialization-vector', + iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, encryption_context_from_request={"key2": "value2"}, - plaintext_data_key=b'plaintext-data-key' + plaintext_data_key=b"plaintext-data-key", ) materials_dict = materials.to_dict() - self.assertEqual(materials_dict['iv'], b'initialization-vector') - self.assertEqual(materials_dict['encrypted_data_keys'], [edk]) - self.assertEqual(materials_dict['encryption_context_stored'], {"key1": "value1"}) - self.assertEqual(materials_dict['encryption_context_from_request'], {"key2": "value2"}) - self.assertEqual(materials_dict['PDK'], b'plaintext-data-key') + self.assertEqual(materials_dict["iv"], b"initialization-vector") + self.assertEqual(materials_dict["encrypted_data_keys"], [edk]) + self.assertEqual(materials_dict["encryption_context_stored"], {"key1": "value1"}) + self.assertEqual(materials_dict["encryption_context_from_request"], {"key2": "value2"}) + self.assertEqual(materials_dict["PDK"], b"plaintext-data-key") + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py index dbbaf431..7e697e92 100644 --- a/test/test_decryption_materials_integration.py +++ b/test/test_decryption_materials_integration.py @@ -3,148 +3,153 @@ import unittest from unittest.mock import MagicMock, patch -from src.s3_encryption.materials.materials import DecryptionMaterials + +from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey from src.s3_encryption.materials.keyring import S3Keyring -from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager +from src.s3_encryption.materials.materials import DecryptionMaterials + class TestDecryptionMaterialsIntegration(unittest.TestCase): def test_keyring_onDecrypt(self): """Test that S3Keyring.onDecrypt properly handles DecryptionMaterials.""" # Create a keyring keyring = S3Keyring() - + # Create an encrypted data key edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) - + # Create decryption materials materials = DecryptionMaterials( - iv=b'initialization-vector', + iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, - encryption_context_from_request={"key2": "value2"} + encryption_context_from_request={"key2": "value2"}, ) - + # Mock the validation method to return the materials - with patch.object(S3Keyring, 'onDecrypt', return_value=materials) as mock_onDecrypt: + with patch.object(S3Keyring, "onDecrypt", return_value=materials) as mock_onDecrypt: # Call onDecrypt result = keyring.onDecrypt(materials, [edk]) - + # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.iv, b"initialization-vector") self.assertEqual(result.encrypted_data_keys, [edk]) self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - + def test_keyring_onDecrypt_default_EC(self): """Test that S3Keyring.onDecrypt properly handles DecryptionMaterials.""" # Create a keyring keyring = S3Keyring() - + # Create an encrypted data key edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) - + # Create decryption materials materials = DecryptionMaterials( - iv=b'initialization-vector', + iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={}, - encryption_context_from_request={} + encryption_context_from_request={}, ) - + # Mock the validation method to return the materials - with patch.object(S3Keyring, 'onDecrypt', return_value=materials) as mock_onDecrypt: + with patch.object(S3Keyring, "onDecrypt", return_value=materials) as mock_onDecrypt: # Call onDecrypt result = keyring.onDecrypt(materials, [edk]) - + # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.iv, b"initialization-vector") self.assertEqual(result.encrypted_data_keys, [edk]) self.assertEqual(result.encryption_context_stored, {}) self.assertEqual(result.encryption_context_from_request, {}) - + def test_cmm_decryptMaterials_with_dict(self): """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles dictionary input.""" # Create a mock keyring keyring = MagicMock() edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) keyring.onDecrypt.return_value = DecryptionMaterials( - iv=b'initialization-vector', + iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, encryption_context_from_request={"key2": "value2"}, - plaintext_data_key=b'plaintext-data-key' + plaintext_data_key=b"plaintext-data-key", ) - + # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - + # Call decryptMaterials with a dictionary - result = cmm.decryptMaterials({ - 'iv': b'initialization-vector', - 'encrypted_data_keys': [edk], - 'encryption_context_stored': {"key1": "value1"}, - 'encryption_context_from_request': {"key2": "value2"} - }) - + result = cmm.decryptMaterials( + { + "iv": b"initialization-vector", + "encrypted_data_keys": [edk], + "encryption_context_stored": {"key1": "value1"}, + "encryption_context_from_request": {"key2": "value2"}, + } + ) + # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.iv, b"initialization-vector") self.assertEqual(result.encrypted_data_keys, [edk]) self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - self.assertEqual(result.plaintext_data_key, b'plaintext-data-key') - + self.assertEqual(result.plaintext_data_key, b"plaintext-data-key") + def test_cmm_decryptMaterials_with_materials(self): """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles DecryptionMaterials input.""" # Create a mock keyring keyring = MagicMock() edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) keyring.onDecrypt.return_value = DecryptionMaterials( - iv=b'initialization-vector', + iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, encryption_context_from_request={"key2": "value2"}, - plaintext_data_key=b'plaintext-data-key' + plaintext_data_key=b"plaintext-data-key", ) - + # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - + # Call decryptMaterials with a DecryptionMaterials instance materials = DecryptionMaterials( - iv=b'initialization-vector', + iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, - encryption_context_from_request={"key2": "value2"} + encryption_context_from_request={"key2": "value2"}, ) result = cmm.decryptMaterials(materials) - + # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b'initialization-vector') + self.assertEqual(result.iv, b"initialization-vector") self.assertEqual(result.encrypted_data_keys, [edk]) self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - self.assertEqual(result.plaintext_data_key, b'plaintext-data-key') + self.assertEqual(result.plaintext_data_key, b"plaintext-data-key") + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_encryption_materials.py b/test/test_encryption_materials.py index b1e6b653..ffa5a449 100644 --- a/test/test_encryption_materials.py +++ b/test/test_encryption_materials.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import unittest -from src.s3_encryption.materials.materials import EncryptionMaterials + from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey +from src.s3_encryption.materials.materials import EncryptionMaterials + class TestEncryptionMaterials(unittest.TestCase): def test_create_encryption_materials(self): @@ -12,46 +14,47 @@ def test_create_encryption_materials(self): self.assertEqual(materials.encryption_context, {}) self.assertIsNone(materials.encrypted_data_key) self.assertIsNone(materials.plaintext_data_key) - + def test_create_with_encryption_context(self): """Test creating an EncryptionMaterials instance with an encryption context.""" encryption_context = {"key1": "value1", "key2": "value2"} materials = EncryptionMaterials(encryption_context=encryption_context) self.assertEqual(materials.encryption_context, encryption_context) - + def test_from_dict(self): """Test creating an EncryptionMaterials instance from a dictionary.""" edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) materials_dict = { - 'encryption_context': {"key1": "value1"}, - 'encrypted_data_key': edk, - 'PDK': b'plaintext-data-key' + "encryption_context": {"key1": "value1"}, + "encrypted_data_key": edk, + "PDK": b"plaintext-data-key", } materials = EncryptionMaterials.from_dict(materials_dict) self.assertEqual(materials.encryption_context, {"key1": "value1"}) self.assertEqual(materials.encrypted_data_key, edk) - self.assertEqual(materials.plaintext_data_key, b'plaintext-data-key') - + self.assertEqual(materials.plaintext_data_key, b"plaintext-data-key") + def test_to_dict(self): """Test converting an EncryptionMaterials instance to a dictionary.""" edk = EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ) materials = EncryptionMaterials( encryption_context={"key1": "value1"}, encrypted_data_key=edk, - plaintext_data_key=b'plaintext-data-key' + plaintext_data_key=b"plaintext-data-key", ) materials_dict = materials.to_dict() - self.assertEqual(materials_dict['encryption_context'], {"key1": "value1"}) - self.assertEqual(materials_dict['encrypted_data_key'], edk) - self.assertEqual(materials_dict['PDK'], b'plaintext-data-key') + self.assertEqual(materials_dict["encryption_context"], {"key1": "value1"}) + self.assertEqual(materials_dict["encrypted_data_key"], edk) + self.assertEqual(materials_dict["PDK"], b"plaintext-data-key") + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_encryption_materials_integration.py b/test/test_encryption_materials_integration.py index 7082bb8f..da8a17b2 100644 --- a/test/test_encryption_materials_integration.py +++ b/test/test_encryption_materials_integration.py @@ -3,29 +3,29 @@ import unittest from unittest.mock import MagicMock, patch -from src.s3_encryption.materials.materials import EncryptionMaterials + +from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey from src.s3_encryption.materials.keyring import S3Keyring -from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager +from src.s3_encryption.materials.materials import EncryptionMaterials + class TestEncryptionMaterialsIntegration(unittest.TestCase): def test_keyring_onEncrypt(self): """Test that S3Keyring.onEncrypt properly handles EncryptionMaterials.""" # Create a keyring keyring = S3Keyring() - + # Create encryption materials - materials = EncryptionMaterials( - encryption_context={"key1": "value1"} - ) - + materials = EncryptionMaterials(encryption_context={"key1": "value1"}) + # Call onEncrypt result = keyring.onEncrypt(materials) - + # Verify the result is an EncryptionMaterials instance self.assertIsInstance(result, EncryptionMaterials) self.assertEqual(result.encryption_context, {"key1": "value1"}) - + def test_cmm_getEncryptionMaterials_with_dict(self): """Test that DefaultCryptoMaterialsManager.getEncryptionMaterials properly handles dictionary input.""" # Create a mock keyring @@ -33,25 +33,25 @@ def test_cmm_getEncryptionMaterials_with_dict(self): keyring.onEncrypt.return_value = EncryptionMaterials( encryption_context={"key1": "value1"}, encrypted_data_key=EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ), - plaintext_data_key=b'plaintext-data-key' + plaintext_data_key=b"plaintext-data-key", ) - + # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - + # Call getEncryptionMaterials with a dictionary result = cmm.getEncryptionMaterials({"encryption_context": {"key1": "value1"}}) - + # Verify the result is an EncryptionMaterials instance self.assertIsInstance(result, EncryptionMaterials) self.assertEqual(result.encryption_context, {"key1": "value1"}) self.assertIsNotNone(result.encrypted_data_key) self.assertIsNotNone(result.plaintext_data_key) - + def test_cmm_getEncryptionMaterials_with_materials(self): """Test that DefaultCryptoMaterialsManager.getEncryptionMaterials properly handles EncryptionMaterials input.""" # Create a mock keyring @@ -59,27 +59,26 @@ def test_cmm_getEncryptionMaterials_with_materials(self): keyring.onEncrypt.return_value = EncryptionMaterials( encryption_context={"key1": "value1"}, encrypted_data_key=EncryptedDataKey( - key_provider_id=b'S3Keyring', + key_provider_id=b"S3Keyring", key_provider_info="kms+context", - encrypted_data_key=b'encrypted-data-key' + encrypted_data_key=b"encrypted-data-key", ), - plaintext_data_key=b'plaintext-data-key' + plaintext_data_key=b"plaintext-data-key", ) - + # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - + # Call getEncryptionMaterials with an EncryptionMaterials instance - materials = EncryptionMaterials( - encryption_context={"key1": "value1"} - ) + materials = EncryptionMaterials(encryption_context={"key1": "value1"}) result = cmm.getEncryptionMaterials(materials) - + # Verify the result is an EncryptionMaterials instance self.assertIsInstance(result, EncryptionMaterials) self.assertEqual(result.encryption_context, {"key1": "value1"}) self.assertIsNotNone(result.encrypted_data_key) self.assertIsNotNone(result.plaintext_data_key) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/test/test_metadata.py b/test/test_metadata.py index 40a813aa..2c26c336 100644 --- a/test/test_metadata.py +++ b/test/test_metadata.py @@ -1,11 +1,11 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import unittest -import sys import os +import sys +import unittest # Add the src directory to the Python path -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) from s3_encryption.metadata import ObjectMetadata @@ -17,67 +17,67 @@ def test_from_dict(self): "x-amz-key-v2": "encrypted-key-data", "x-amz-wrap-alg": "kms+context", "x-amz-iv": "base64-encoded-iv", - "x-amz-cek-alg": "AES/GCM/NoPadding" + "x-amz-cek-alg": "AES/GCM/NoPadding", } - + # Create an ObjectMetadata instance from the dictionary metadata = ObjectMetadata.from_dict(metadata_dict) - + # Verify that the fields were populated correctly self.assertEqual(metadata.encrypted_data_key_v2, "encrypted-key-data") self.assertEqual(metadata.encrypted_data_key_algorithm, "kms+context") self.assertEqual(metadata.content_iv, "base64-encoded-iv") self.assertEqual(metadata.content_cipher, "AES/GCM/NoPadding") - + # Verify that fields not in the dictionary are None self.assertIsNone(metadata.encrypted_data_key_v1) self.assertIsNone(metadata.encrypted_data_key_context) # Note: content_cipher_tag_length is None because it's not in the input dictionary self.assertIsNone(metadata.content_cipher_tag_length) self.assertIsNone(metadata.instruction_file) - + def test_to_dict(self): # Create an ObjectMetadata instance with some fields set metadata = ObjectMetadata( encrypted_data_key_v2="encrypted-key-data", encrypted_data_key_algorithm="kms+context", content_iv="base64-encoded-iv", - content_cipher="AES/GCM/NoPadding" + content_cipher="AES/GCM/NoPadding", ) - + # Convert to dictionary metadata_dict = metadata.to_dict() - + # Verify that the dictionary contains the expected keys and values self.assertEqual(metadata_dict["x-amz-key-v2"], "encrypted-key-data") self.assertEqual(metadata_dict["x-amz-wrap-alg"], "kms+context") self.assertEqual(metadata_dict["x-amz-iv"], "base64-encoded-iv") self.assertEqual(metadata_dict["x-amz-cek-alg"], "AES/GCM/NoPadding") - + # Verify that fields that are None are not included in the dictionary self.assertNotIn("x-amz-key", metadata_dict) self.assertNotIn("x-amz-matdesc", metadata_dict) # Note: content_cipher_tag_length has a default value of "128" self.assertEqual(metadata_dict.get("x-amz-tag-len"), "128") self.assertNotIn("x-amz-crypto-instr-file", metadata_dict) - + def test_roundtrip(self): # Create a metadata dictionary original_dict = { "x-amz-key-v2": "encrypted-key-data", "x-amz-wrap-alg": "kms+context", "x-amz-iv": "base64-encoded-iv", - "x-amz-cek-alg": "AES/GCM/NoPadding" + "x-amz-cek-alg": "AES/GCM/NoPadding", } - + # Convert to ObjectMetadata and back to dictionary metadata = ObjectMetadata.from_dict(original_dict) result_dict = metadata.to_dict() - + # Remove the tag length field which has a default value if "x-amz-tag-len" in result_dict: result_dict.pop("x-amz-tag-len") - + # Verify that the result matches the original self.assertEqual(result_dict, original_dict) From 454c0ebf6d64afff30cd5fccd9d0aed61aed5571 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:15:50 -0700 Subject: [PATCH 15/36] address feedback (ruff, uv, etc) --- Makefile | 21 +- pyproject.toml | 62 +-- requirements.txt | 12 + src/s3_encryption/__init__.py | 3 +- .../materials/crypto_materials_manager.py | 13 +- .../materials/encrypted_data_key.py | 3 +- src/s3_encryption/materials/keyring.py | 18 +- src/s3_encryption/materials/kms_keyring.py | 12 +- src/s3_encryption/materials/materials.py | 44 +- src/s3_encryption/metadata.py | 31 +- src/s3_encryption/pipelines.py | 13 +- test/integration/test_i_s3_encryption.py | 1 - test/test_encryption_materials_integration.py | 2 +- uv.lock | 380 ++++++++++++++++++ 14 files changed, 494 insertions(+), 121 deletions(-) create mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/Makefile b/Makefile index 15dd281a..65578384 100644 --- a/Makefile +++ b/Makefile @@ -5,30 +5,31 @@ all: lint test # Install dependencies install: - poetry install + uv pip install -e ".[dev,test]" # Run linting checks lint: - poetry run black --check . - poetry run isort --check . - # Allow flake8 to fail for now as we're gradually adopting linting standards - poetry run flake8 src/ test/ || true + uv run black --check . + uv run isort --check . + # Allow ruff to fail for now as we're gradually adopting linting standards + uv run ruff check src/ test/ || true -# Format code with Black and isort +# Format code with Black, isort, and Ruff format: - poetry run black . - poetry run isort . + uv run black . + uv run isort . + uv run ruff check --fix src/ test/ # Run all tests test: test-unit test-integration # Run unit tests test-unit: - poetry run pytest test/ --verbose + uv run pytest test/ --verbose # Run integration tests test-integration: - poetry run pytest test/integration/ --verbose + uv run pytest test/integration/ --verbose # Clean up cache files clean: diff --git a/pyproject.toml b/pyproject.toml index 89a86e17..a613bc12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,43 +1,55 @@ -[tool.poetry] +[project] name = "amazon-s3-encryption-client-python" version = "0.1.0" description = "This library provides an S3 client that supports client-side encryption." -authors = ["AWS Crypto Tools "] -license = "Apache-2.0" +authors = [ + {name = "AWS Crypto Tools", email = "aws-crypto-tools@amazon.com"} +] +license = {text = "Apache-2.0"} readme = "README.md" -packages = [{include = "s3_encryption", from = "src"}] - -[tool.poetry.dependencies] -python = "^3.11" -boto3 = "^1.37.2" -# There is a newer version, but MPL wants this one. -cryptography = "^43.0.1" -aws-cryptographic-material-providers = "^1.7.4" -attrs = "^25.1.0" - -[tool.poetry.group.dev.dependencies] -pytest = "^8.4.1" -black = "^24.3.0" -flake8 = "^7.0.0" -flake8-docstrings = "^1.7.0" -isort = "^5.13.2" +requires-python = ">=3.11" +dependencies = [ + "boto3>=1.37.2", + "cryptography>=45.0.6", + "attrs>=25.1.0", +] +[project.optional-dependencies] +test = [ + "pytest>=8.4.1", +] +dev = [ + "black>=24.3.0", + "ruff>=0.3.0", + "isort>=5.13.2", +] [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/s3_encryption"] [tool.black] line-length = 100 target-version = ["py311"] include = '\.pyi?$' -[tool.flake8] -max-line-length = 100 +[tool.ruff] +line-length = 100 +target-version = "py311" exclude = [".git", "__pycache__", "build", "dist"] + +[tool.ruff.lint] +# Enable all rules by default, then configure specific rule settings below +select = ["E", "F", "W", "I", "N", "D", "UP", "B", "A", "C4", "PT", "RET", "SIM", "ARG", "ERA"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.mccabe] max-complexity = 10 -ignore = ["E203", "W503"] # E203 and W503 conflict with Black -docstring-convention = "google" [tool.isort] profile = "black" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..423d8c8d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +# Main dependencies +boto3>=1.37.2 +cryptography>=45.0.6 +attrs>=25.1.0 + +# Test dependencies +pytest>=8.4.1 + +# Development dependencies +black>=24.3.0 +ruff>=0.3.0 +isort>=5.13.2 diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index ab0a77fe..fb633945 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -16,8 +16,7 @@ @define class S3EncryptionClientConfig: - """ - Configuration object for the S3 Encryption Client + """Configuration object for the S3 Encryption Client """ keyring: AbstractKeyring diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index c2d67edf..c668a53e 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -1,6 +1,5 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Dict, List, Union from attrs import define @@ -11,8 +10,7 @@ # API Stub for CMM class AbstractCryptoMaterialsManager: def getEncryptionMaterials(self, encMatsRequest): - """ - Get encryption materials from the keyring. + """Get encryption materials from the keyring. Args: encMatsRequest (Dict[str, Any] or EncryptionMaterials): Request containing encryption parameters @@ -23,8 +21,7 @@ def getEncryptionMaterials(self, encMatsRequest): raise NotImplementedError def decryptMaterials(self, decMatsRequest): - """ - Decrypt materials using the keyring. + """Decrypt materials using the keyring. Args: decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters @@ -40,8 +37,7 @@ class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): keyring: AbstractKeyring def getEncryptionMaterials(self, encMatsRequest): - """ - Get encryption materials from the keyring. + """Get encryption materials from the keyring. Args: encMatsRequest (Dict[str, Any]): Request containing encryption parameters @@ -60,8 +56,7 @@ def getEncryptionMaterials(self, encMatsRequest): return self.keyring.onEncrypt(materials) def decryptMaterials(self, decMatsRequest): - """ - Decrypt materials using the keyring. + """Decrypt materials using the keyring. Args: decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters diff --git a/src/s3_encryption/materials/encrypted_data_key.py b/src/s3_encryption/materials/encrypted_data_key.py index 0dbfa08f..51d47c3b 100644 --- a/src/s3_encryption/materials/encrypted_data_key.py +++ b/src/s3_encryption/materials/encrypted_data_key.py @@ -5,8 +5,7 @@ @define class EncryptedDataKey: - """ - Class representing an encrypted data key. + """Class representing an encrypted data key. An encrypted data key contains information about the key provider and the encrypted data key itself. diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 98e4f6ab..f840bce4 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -1,9 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import List, Optional -from attrs import define, field +from attrs import define from ..exceptions import S3EncryptionClientError from .materials import DecryptionMaterials, EncryptionMaterials @@ -18,8 +17,7 @@ class AbstractKeyring: # enableLegacyWrappingAlgorithms: bool = field(default=False) def onEncrypt(self, encMaterials): - """ - Process encryption materials. + """Process encryption materials. Args: encMaterials (EncryptionMaterials): Encryption materials to process @@ -30,8 +28,7 @@ def onEncrypt(self, encMaterials): raise NotImplementedError def onDecrypt(self, decMaterials, encrypted_data_keys=None): - """ - Decrypt one of the encrypted data keys and update decMaterials. + """Decrypt one of the encrypted data keys and update decMaterials. Args: decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials @@ -45,16 +42,14 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): @define class S3Keyring(AbstractKeyring): - """ - Base class for S3 encryption keyrings that provides common validation logic. + """Base class for S3 encryption keyrings that provides common validation logic. """ # Ideally this would be set, but attrs doesn't play nice # enable_legacy_wrapping_algorithms: bool = field(default=False) def onEncrypt(self, encMaterials): - """ - Validate encryption materials before encryption. + """Validate encryption materials before encryption. Args: encMaterials (EncryptionMaterials or dict): Encryption materials @@ -79,8 +74,7 @@ def onEncrypt(self, encMaterials): return encMaterials def onDecrypt(self, decMaterials, encrypted_data_keys=None): - """ - Validate decryption materials before decryption. + """Validate decryption materials before decryption. Args: decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 4ba5e31b..9559ff29 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -1,13 +1,11 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import List, Optional from attrs import define, field from ..exceptions import S3EncryptionClientError from .encrypted_data_key import EncryptedDataKey from .keyring import S3Keyring -from .materials import DecryptionMaterials, EncryptionMaterials KMS_CONTEXT_DEFAULT_KEY = "aws:x-amz-cek-alg" KMS_V1_DEFAULT_KEY = "kms_cmk_id" @@ -20,8 +18,7 @@ class KmsKeyring(S3Keyring): enable_legacy_wrapping_algorithms: bool = field(default=False) def onEncrypt(self, encMaterials): - """ - Process encryption materials using KMS. + """Process encryption materials using KMS. Args: encMaterials (EncryptionMaterials): Encryption materials to process @@ -49,12 +46,11 @@ def onEncrypt(self, encMaterials): encMaterials.encrypted_data_key = encrypted_data_key encMaterials.plaintext_data_key = response["Plaintext"] return encMaterials - except Exception as e: + except Exception: raise def onDecrypt(self, decMaterials, encrypted_data_keys=None): - """ - Decrypt one of the encrypted data keys and update decMaterials. + """Decrypt one of the encrypted data keys and update decMaterials. Args: decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials @@ -129,5 +125,5 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): raise last_exception else: raise S3EncryptionClientError("Failed to decrypt any of the encrypted data keys") - except Exception as e: + except Exception: raise diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py index ffab848d..1ab235da 100644 --- a/src/s3_encryption/materials/materials.py +++ b/src/s3_encryption/materials/materials.py @@ -1,6 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Dict, List, Optional +from typing import Any from attrs import define, field @@ -9,8 +9,7 @@ @define class EncryptionMaterials: - """ - Class representing encryption materials for S3 encryption. + """Class representing encryption materials for S3 encryption. This class provides a structured way to handle encryption materials with fields corresponding to the data needed for encryption operations. @@ -21,14 +20,13 @@ class EncryptionMaterials: plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) """ - encryption_context: Dict[str, str] = field(factory=dict) - encrypted_data_key: Optional[EncryptedDataKey] = field(default=None) - plaintext_data_key: Optional[bytes] = field(default=None) + encryption_context: dict[str, str] = field(factory=dict) + encrypted_data_key: EncryptedDataKey | None = field(default=None) + plaintext_data_key: bytes | None = field(default=None) @classmethod - def from_dict(cls, materials_dict: Dict[str, Any]) -> "EncryptionMaterials": - """ - Create an EncryptionMaterials instance from a dictionary. + def from_dict(cls, materials_dict: dict[str, Any]) -> "EncryptionMaterials": + """Create an EncryptionMaterials instance from a dictionary. Args: materials_dict (Dict[str, Any]): Dictionary containing encryption materials @@ -42,9 +40,8 @@ def from_dict(cls, materials_dict: Dict[str, Any]) -> "EncryptionMaterials": plaintext_data_key=materials_dict.get("PDK"), ) - def to_dict(self) -> Dict[str, Any]: - """ - Convert the EncryptionMaterials instance to a dictionary. + def to_dict(self) -> dict[str, Any]: + """Convert the EncryptionMaterials instance to a dictionary. Returns: Dict[str, Any]: Dictionary containing encryption materials @@ -65,8 +62,7 @@ def to_dict(self) -> Dict[str, Any]: @define class DecryptionMaterials: - """ - Class representing decryption materials for S3 encryption. + """Class representing decryption materials for S3 encryption. This class provides a structured way to handle decryption materials with fields corresponding to the data needed for decryption operations. @@ -79,16 +75,15 @@ class DecryptionMaterials: plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) """ - iv: Optional[bytes] = field(default=None) - encrypted_data_keys: List[EncryptedDataKey] = field(factory=list) - encryption_context_stored: Dict[str, str] = field(factory=dict) - encryption_context_from_request: Dict[str, str] = field(factory=dict) - plaintext_data_key: Optional[bytes] = field(default=None) + iv: bytes | None = field(default=None) + encrypted_data_keys: list[EncryptedDataKey] = field(factory=list) + encryption_context_stored: dict[str, str] = field(factory=dict) + encryption_context_from_request: dict[str, str] = field(factory=dict) + plaintext_data_key: bytes | None = field(default=None) @classmethod - def from_dict(cls, materials_dict: Dict[str, Any]) -> "DecryptionMaterials": - """ - Create a DecryptionMaterials instance from a dictionary. + def from_dict(cls, materials_dict: dict[str, Any]) -> "DecryptionMaterials": + """Create a DecryptionMaterials instance from a dictionary. Args: materials_dict (Dict[str, Any]): Dictionary containing decryption materials @@ -106,9 +101,8 @@ def from_dict(cls, materials_dict: Dict[str, Any]) -> "DecryptionMaterials": plaintext_data_key=materials_dict.get("PDK"), ) - def to_dict(self) -> Dict[str, Any]: - """ - Convert the DecryptionMaterials instance to a dictionary. + def to_dict(self) -> dict[str, Any]: + """Convert the DecryptionMaterials instance to a dictionary. Returns: Dict[str, Any]: Dictionary containing decryption materials diff --git a/src/s3_encryption/metadata.py b/src/s3_encryption/metadata.py index ce963328..c59370bb 100644 --- a/src/s3_encryption/metadata.py +++ b/src/s3_encryption/metadata.py @@ -1,15 +1,14 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import json -from typing import Any, Dict, Optional +from typing import Any from attrs import define, field @define class ObjectMetadata: - """ - Class representing metadata for encrypted S3 objects. + """Class representing metadata for encrypted S3 objects. This class provides a structured way to handle encryption metadata with fields corresponding to standard S3 encryption headers. @@ -26,21 +25,21 @@ class ObjectMetadata: """ # The encrypted data key (legacy format) - encrypted_data_key_v1: Optional[str] = field(default=None) + encrypted_data_key_v1: str | None = field(default=None) # The encrypted data key (current format) - encrypted_data_key_v2: Optional[str] = field(default=None) + encrypted_data_key_v2: str | None = field(default=None) # The algorithm used to encrypt the data key (e.g. AES/GCM or kms+context) - encrypted_data_key_algorithm: Optional[str] = field(default=None) + encrypted_data_key_algorithm: str | None = field(default=None) # The encryption context used for the data key - encrypted_data_key_context: Optional[dict] = field(default=None) + encrypted_data_key_context: dict | None = field(default=None) # The initialization vector used for content encryption - content_iv: Optional[str] = field(default=None) + content_iv: str | None = field(default=None) # The cipher algorithm used for content encryption (e.g. AES/GCM/NoPadding) - content_cipher: Optional[str] = field(default=None) + content_cipher: str | None = field(default=None) # The length of the authentication tag - content_cipher_tag_length: Optional[str] = field(default="128") + content_cipher_tag_length: str | None = field(default="128") # Marker for instruction files - instruction_file: Optional[str] = field(default=None) + instruction_file: str | None = field(default=None) # Constants for metadata keys ENCRYPTED_DATA_KEY_V1 = "x-amz-key" @@ -53,9 +52,8 @@ class ObjectMetadata: INSTRUCTION_FILE = "x-amz-crypto-instr-file" @classmethod - def from_dict(cls, metadata_dict: Dict[str, Any]) -> "ObjectMetadata": - """ - Create an ObjectMetadata instance from a dictionary. + def from_dict(cls, metadata_dict: dict[str, Any]) -> "ObjectMetadata": + """Create an ObjectMetadata instance from a dictionary. Args: metadata_dict (Dict[str, Any]): Dictionary containing metadata keys and values @@ -81,9 +79,8 @@ def from_dict(cls, metadata_dict: Dict[str, Any]) -> "ObjectMetadata": instruction_file=metadata_dict.get(cls.INSTRUCTION_FILE), ) - def to_dict(self) -> Dict[str, str]: - """ - Convert the ObjectMetadata instance to a dictionary. + def to_dict(self) -> dict[str, str]: + """Convert the ObjectMetadata instance to a dictionary. Returns: Dict[str, str]: Dictionary containing non-None metadata values diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index c207fd45..44aa4dfa 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import base64 import os -from typing import Any, Dict, List, Optional, Union from attrs import define, field from cryptography.hazmat.primitives.ciphers.aead import AESGCM @@ -15,8 +14,7 @@ @define class PutEncryptedObjectPipeline: - """ - Pipeline for encrypting objects before they are put into S3. + """Pipeline for encrypting objects before they are put into S3. This pipeline handles only the encryption process for S3 objects. The actual S3 API calls are handled by the S3EncryptionClient. @@ -25,8 +23,7 @@ class PutEncryptedObjectPipeline: cmm: AbstractCryptoMaterialsManager = field() def encrypt(self, plaintext, encryption_context=None): - """ - Encrypt the data before it is stored in S3. + """Encrypt the data before it is stored in S3. Args: data (bytes or str): The data to be encrypted @@ -80,8 +77,7 @@ def encrypt(self, plaintext, encryption_context=None): @define class GetEncryptedObjectPipeline: - """ - Pipeline for decrypting objects after they are retrieved from S3. + """Pipeline for decrypting objects after they are retrieved from S3. This pipeline handles only the decryption process for S3 objects. The actual S3 API calls are handled by the S3EncryptionClient. @@ -90,8 +86,7 @@ class GetEncryptedObjectPipeline: cmm: AbstractCryptoMaterialsManager = field() def decrypt(self, response, encryption_context={}): - """ - Decrypt the data after it is retrieved from S3. + """Decrypt the data after it is retrieved from S3. Args: response (dict): The response from S3 containing the encrypted data and metadata diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index d956f321..16446b1d 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -4,7 +4,6 @@ from datetime import datetime import boto3 - from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig from s3_encryption.materials.kms_keyring import KmsKeyring diff --git a/test/test_encryption_materials_integration.py b/test/test_encryption_materials_integration.py index da8a17b2..be16662d 100644 --- a/test/test_encryption_materials_integration.py +++ b/test/test_encryption_materials_integration.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..e2aaa475 --- /dev/null +++ b/uv.lock @@ -0,0 +1,380 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "amazon-s3-encryption-client-python" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "attrs" }, + { name = "boto3" }, + { name = "cryptography" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "ruff" }, +] +test = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "attrs", specifier = ">=25.1.0" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=24.3.0" }, + { name = "boto3", specifier = ">=1.37.2" }, + { name = "cryptography", specifier = ">=45.0.6" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.2" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, +] +provides-extras = ["test", "dev"] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/f31556d817e872c2723196a34b197d971d78297b22b8bae0ae6d93f7f9c1/boto3-1.40.7.tar.gz", hash = "sha256:61b15f70761f1eadd721c6ba41a92658f003eaaef09500ca7642f5ae68ec8945", size = 111989, upload-time = "2025-08-11T19:20:45.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/e3/f2a77f4809ffe4e896c2e6186db88333ae980f52a91b28e9fd068d8f5506/boto3-1.40.7-py3-none-any.whl", hash = "sha256:8727cac601a679d2885dc78b8119a0548bbbe04e49b72f7d94021a629154c080", size = 140061, upload-time = "2025-08-11T19:20:43.173Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/d7/5e559918410b259c1e54a4646ff39c56433e1c9cefa5e66ab0f06716cee8/botocore-1.40.7.tar.gz", hash = "sha256:33793696680cf3a0c4b5ace4f9070c67c4d4fcb19c999fd85cfee55de3dcf913", size = 14318282, upload-time = "2025-08-11T19:20:33.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fa/bb7ec68b24d1b4678d341a305cbfed78a593e6383c86a70727410e4d0e11/botocore-1.40.7-py3-none-any.whl", hash = "sha256:a06956f3d7222e80ef6ae193608f358c3b7898e1a2b88553479d8f9737fbb03e", size = 13981488, upload-time = "2025-08-11T19:20:27.303Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] From 351431a780ff088d6054b8bcf2fee74aa5e7bfff Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:21:53 -0700 Subject: [PATCH 16/36] GHA stuff --- .github/workflows/lint.yml | 36 ++++++++++++++++++++++++++++++++++++ .github/workflows/main.yml | 4 ++++ .github/workflows/test.yml | 14 +++++--------- 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..3f5d091a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Uv + run: pip install uv + + - name: Install dependencies + run: uv pip install -e ".[dev,test]" + + - name: Run Black + run: uv run black --check . + + - name: Run isort + run: uv run isort --check . + + - name: Run Ruff + # Allow ruff to fail for now as we're gradually adopting linting standards + run: uv run ruff check src/ test/ || true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b7396de4..e10b7d0d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,10 @@ on: type: string jobs: + lint: + name: Lint + uses: ./.github/workflows/lint.yml + run-tests: name: Run Tests uses: ./.github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c60d4a20..68a0d1e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,15 +26,11 @@ jobs: with: python-version: ${{ inputs.python-version || '3.11' }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 1.7.1 - virtualenvs-create: true - virtualenvs-in-project: true + - name: Install Uv + run: pip install uv - name: Install dependencies - run: poetry install + run: uv pip install -e ".[dev,test]" - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 @@ -43,10 +39,10 @@ jobs: aws-region: us-west-2 - name: Run unit tests - run: poetry run pytest test/ --verbose + run: uv run pytest test/ --verbose - name: Run integration tests - run: poetry run pytest test/integration/ --verbose + run: uv run pytest test/integration/ --verbose env: CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} From c15f2da80e9efec778e6a8ee94cda2b9deb5f9f2 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:25:09 -0700 Subject: [PATCH 17/36] uv venv --- .github/workflows/lint.yml | 3 +++ .github/workflows/test.yml | 3 +++ Makefile | 1 + 3 files changed, 7 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3f5d091a..f4cd3512 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,6 +22,9 @@ jobs: - name: Install Uv run: pip install uv + - name: Create virtual environment + run: uv venv + - name: Install dependencies run: uv pip install -e ".[dev,test]" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68a0d1e1..8b07e40a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,9 @@ jobs: - name: Install Uv run: pip install uv + - name: Create virtual environment + run: uv venv + - name: Install dependencies run: uv pip install -e ".[dev,test]" diff --git a/Makefile b/Makefile index 65578384..fb1a83b0 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ all: lint test # Install dependencies install: + uv venv uv pip install -e ".[dev,test]" # Run linting checks From 5e3f122224c7c796051011ff2832a0aca5d24b18 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:32:49 -0700 Subject: [PATCH 18/36] black --- src/s3_encryption/__init__.py | 3 +-- src/s3_encryption/materials/keyring.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index fb633945..c19ca28d 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -16,8 +16,7 @@ @define class S3EncryptionClientConfig: - """Configuration object for the S3 Encryption Client - """ + """Configuration object for the S3 Encryption Client""" keyring: AbstractKeyring cmm: AbstractCryptoMaterialsManager = field() diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index f840bce4..aed20e96 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -42,8 +42,7 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): @define class S3Keyring(AbstractKeyring): - """Base class for S3 encryption keyrings that provides common validation logic. - """ + """Base class for S3 encryption keyrings that provides common validation logic.""" # Ideally this would be set, but attrs doesn't play nice # enable_legacy_wrapping_algorithms: bool = field(default=False) From dac7ab7c0d6a47ab1e4574bb1d6a919af4f5e1aa Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:47:02 -0700 Subject: [PATCH 19/36] only ruff, remove isort --- Makefile | 4 +--- pyproject.toml | 7 ++----- test/integration/test_i_s3_encryption.py | 1 + uv.lock | 11 ----------- 4 files changed, 4 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index fb1a83b0..80baca1d 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,12 @@ install: # Run linting checks lint: uv run black --check . - uv run isort --check . # Allow ruff to fail for now as we're gradually adopting linting standards uv run ruff check src/ test/ || true -# Format code with Black, isort, and Ruff +# Format code with Black and Ruff format: uv run black . - uv run isort . uv run ruff check --fix src/ test/ # Run all tests diff --git a/pyproject.toml b/pyproject.toml index a613bc12..883b6914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ test = [ dev = [ "black>=24.3.0", "ruff>=0.3.0", - "isort>=5.13.2", ] [build-system] @@ -51,7 +50,5 @@ convention = "google" [tool.ruff.lint.mccabe] max-complexity = 10 -[tool.isort] -profile = "black" -line_length = 100 -known_first_party = ["s3_encryption"] +[tool.ruff.lint.isort] +known-first-party = ["s3_encryption"] diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index 16446b1d..d956f321 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -4,6 +4,7 @@ from datetime import datetime import boto3 + from s3_encryption import S3EncryptionClient, S3EncryptionClientConfig from s3_encryption.materials.kms_keyring import KmsKeyring diff --git a/uv.lock b/uv.lock index e2aaa475..ebc7a7e9 100644 --- a/uv.lock +++ b/uv.lock @@ -15,7 +15,6 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "black" }, - { name = "isort" }, { name = "ruff" }, ] test = [ @@ -28,7 +27,6 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=24.3.0" }, { name = "boto3", specifier = ">=1.37.2" }, { name = "cryptography", specifier = ">=45.0.6" }, - { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.2" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, ] @@ -215,15 +213,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, -] - [[package]] name = "jmespath" version = "1.0.1" From 3cd450bc552b516d9b8e3b338706196e79227a76 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:51:04 -0700 Subject: [PATCH 20/36] use makefile --- .github/workflows/lint.yml | 19 ++++--------------- .github/workflows/test.yml | 9 +++------ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f4cd3512..8fc3bdc7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,18 +22,7 @@ jobs: - name: Install Uv run: pip install uv - - name: Create virtual environment - run: uv venv - - - name: Install dependencies - run: uv pip install -e ".[dev,test]" - - - name: Run Black - run: uv run black --check . - - - name: Run isort - run: uv run isort --check . - - - name: Run Ruff - # Allow ruff to fail for now as we're gradually adopting linting standards - run: uv run ruff check src/ test/ || true + - name: Install dependencies and run linting + run: | + make install + make lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b07e40a..0bdc88de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,11 +29,8 @@ jobs: - name: Install Uv run: pip install uv - - name: Create virtual environment - run: uv venv - - name: Install dependencies - run: uv pip install -e ".[dev,test]" + run: make install - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 @@ -42,10 +39,10 @@ jobs: aws-region: us-west-2 - name: Run unit tests - run: uv run pytest test/ --verbose + run: make test-unit - name: Run integration tests - run: uv run pytest test/integration/ --verbose + run: make test-integration env: CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} From be51b702cb4ce0c7ff41e69759757e655d7ce2b0 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 13:57:07 -0700 Subject: [PATCH 21/36] fix CI --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8fc3bdc7..16057711 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ name: Lint on: push: branches: [ main ] - pull_request: + workflow_call: workflow_dispatch: jobs: From 8bbae1a1a324e48d7567e98395fc2c92840eedc0 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 14:02:29 -0700 Subject: [PATCH 22/36] split out integ tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 80baca1d..47ace280 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ test: test-unit test-integration # Run unit tests test-unit: - uv run pytest test/ --verbose + uv run pytest test/ --ignore=test/integration/ --verbose # Run integration tests test-integration: From 090c8a437af03c579540fdb773d514bdafc17130 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 14:41:37 -0700 Subject: [PATCH 23/36] match boto3, use abc --- src/s3_encryption/__init__.py | 28 ++++++++++++------- .../materials/crypto_materials_manager.py | 9 ++++-- src/s3_encryption/materials/keyring.py | 9 ++++-- test/integration/test_i_s3_encryption.py | 2 +- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index c19ca28d..163ca64e 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -31,23 +31,28 @@ class S3EncryptionClient: wrapped_s3_client = field() config: S3EncryptionClientConfig = field() - # TODO: rename Data-> Body to match boto - def put_object(self, Bucket, Key, Data, EncryptionContext=None, **kwargs): + def put_object(self, **kwargs): + # Extract required parameters from kwargs + bucket = kwargs.pop("Bucket") + key = kwargs.pop("Key") + body = kwargs.pop("Body") + encryption_context = kwargs.pop("EncryptionContext", None) + # Create a pipeline for this operation pipeline = PutEncryptedObjectPipeline(self.config.cmm) # Encrypt the data using the pipeline - data_bytes = Data + data_bytes = body # We probably just shouldn't support strings, use utf8 for now # TODO: look deeper into this, what does normal boto3 do? - if type(Data) == str: - data_bytes = Data.encode("utf-8") + if type(body) == str: + data_bytes = body.encode("utf-8") encrypted_data, encryption_metadata = pipeline.encrypt( - data_bytes, encryption_context=EncryptionContext + data_bytes, encryption_context=encryption_context ) # Add encryption metadata to the request parameters - params = {"Bucket": Bucket, "Key": Key, "Body": encrypted_data, **kwargs} + params = {"Bucket": bucket, "Key": key, "Body": encrypted_data, **kwargs} # Add encryption metadata to the parameters if encryption_metadata: @@ -58,8 +63,11 @@ def put_object(self, Bucket, Key, Data, EncryptionContext=None, **kwargs): return self.wrapped_s3_client.put_object(**params) - def get_object(self, EncryptionContext=None, **kwargs): - # try just straight kwargs + def get_object(self, **kwargs): + # Extract encryption context if provided + encryption_context = kwargs.pop("EncryptionContext", None) + + # Create params for the S3 client params = {**kwargs} # Get the encrypted object from S3 @@ -70,7 +78,7 @@ def get_object(self, EncryptionContext=None, **kwargs): # Decrypt the data using the pipeline decrypted_data = pipeline.decrypt( - response, EncryptionContext + response, encryption_context ) # encrypted_data, encryption_metadata) # Create a new streaming body with the decrypted data diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index c668a53e..c4247292 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -1,6 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import abc from attrs import define from .keyring import AbstractKeyring @@ -8,7 +9,8 @@ # API Stub for CMM -class AbstractCryptoMaterialsManager: +class AbstractCryptoMaterialsManager(abc.ABC): + @abc.abstractmethod def getEncryptionMaterials(self, encMatsRequest): """Get encryption materials from the keyring. @@ -18,8 +20,9 @@ def getEncryptionMaterials(self, encMatsRequest): Returns: EncryptionMaterials: The encryption materials """ - raise NotImplementedError + pass + @abc.abstractmethod def decryptMaterials(self, decMatsRequest): """Decrypt materials using the keyring. @@ -29,7 +32,7 @@ def decryptMaterials(self, decMatsRequest): Returns: DecryptionMaterials: The decryption materials """ - raise NotImplementedError + pass @define diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index aed20e96..fe32425b 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 +import abc from attrs import define from ..exceptions import S3EncryptionClientError @@ -9,13 +10,14 @@ @define -class AbstractKeyring: +class AbstractKeyring(abc.ABC): # Ideally, all keyrings would inherit this field. # However, attrs doesn't allow us to set a default here, # when inheriting keyrings have optional fields. # Even without a default it doesn't seem to play nice with attrs. # enableLegacyWrappingAlgorithms: bool = field(default=False) + @abc.abstractmethod def onEncrypt(self, encMaterials): """Process encryption materials. @@ -25,8 +27,9 @@ def onEncrypt(self, encMaterials): Returns: EncryptionMaterials: The processed encryption materials """ - raise NotImplementedError + pass + @abc.abstractmethod def onDecrypt(self, decMaterials, encrypted_data_keys=None): """Decrypt one of the encrypted data keys and update decMaterials. @@ -37,7 +40,7 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): Returns: DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) """ - raise NotImplementedError + pass @define diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index d956f321..f2c4bcf4 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -28,7 +28,7 @@ def test_simple_roundtrip(): wrapped_client = boto3.client("s3") config = S3EncryptionClientConfig(keyring) s3ec = S3EncryptionClient(wrapped_client, config) - s3ec.put_object(Bucket=bucket, Key=key, Data=data) + s3ec.put_object(Bucket=bucket, Key=key, Body=data) get_req = {"Bucket": bucket, "Key": key} response = s3ec.get_object(**get_req) output = response["Body"].read().decode("utf-8") From 15d21444c5525f251433d2c6159103115716dc1d Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 15:05:45 -0700 Subject: [PATCH 24/36] snake case renaming --- src/s3_encryption/__init__.py | 2 +- .../materials/crypto_materials_manager.py | 33 +++++++------- src/s3_encryption/materials/keyring.py | 43 ++++++++++--------- src/s3_encryption/materials/kms_keyring.py | 38 ++++++++-------- src/s3_encryption/pipelines.py | 4 +- test/test_decryption_materials_integration.py | 40 ++++++++--------- test/test_encryption_materials_integration.py | 28 ++++++------ 7 files changed, 95 insertions(+), 93 deletions(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index 163ca64e..0b56454b 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -66,7 +66,7 @@ def put_object(self, **kwargs): def get_object(self, **kwargs): # Extract encryption context if provided encryption_context = kwargs.pop("EncryptionContext", None) - + # Create params for the S3 client params = {**kwargs} diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index c4247292..06965eff 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import abc + from attrs import define from .keyring import AbstractKeyring @@ -11,11 +12,11 @@ # API Stub for CMM class AbstractCryptoMaterialsManager(abc.ABC): @abc.abstractmethod - def getEncryptionMaterials(self, encMatsRequest): + def get_encryption_materials(self, enc_mats_request): """Get encryption materials from the keyring. Args: - encMatsRequest (Dict[str, Any] or EncryptionMaterials): Request containing encryption parameters + enc_mats_request (Dict[str, Any] or EncryptionMaterials): Request containing encryption parameters Returns: EncryptionMaterials: The encryption materials @@ -23,11 +24,11 @@ def getEncryptionMaterials(self, encMatsRequest): pass @abc.abstractmethod - def decryptMaterials(self, decMatsRequest): + def decrypt_materials(self, dec_mats_request): """Decrypt materials using the keyring. Args: - decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters + dec_mats_request (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters Returns: DecryptionMaterials: The decryption materials @@ -39,39 +40,39 @@ def decryptMaterials(self, decMatsRequest): class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): keyring: AbstractKeyring - def getEncryptionMaterials(self, encMatsRequest): + def get_encryption_materials(self, enc_mats_request): """Get encryption materials from the keyring. Args: - encMatsRequest (Dict[str, Any]): Request containing encryption parameters + enc_mats_request (Dict[str, Any]): Request containing encryption parameters Returns: EncryptionMaterials: The encryption materials """ # Convert dictionary to EncryptionMaterials if needed - if isinstance(encMatsRequest, dict): + if isinstance(enc_mats_request, dict): materials = EncryptionMaterials( - encryption_context=encMatsRequest.get("encryption_context", {}) + encryption_context=enc_mats_request.get("encryption_context", {}) ) else: - materials = encMatsRequest + materials = enc_mats_request - return self.keyring.onEncrypt(materials) + return self.keyring.on_encrypt(materials) - def decryptMaterials(self, decMatsRequest): + def decrypt_materials(self, dec_mats_request): """Decrypt materials using the keyring. Args: - decMatsRequest (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters + dec_mats_request (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters Returns: DecryptionMaterials: The decryption materials """ # Convert dictionary to DecryptionMaterials if needed - if isinstance(decMatsRequest, dict): - materials = DecryptionMaterials.from_dict(decMatsRequest) + if isinstance(dec_mats_request, dict): + materials = DecryptionMaterials.from_dict(dec_mats_request) else: - materials = decMatsRequest + materials = dec_mats_request encrypted_data_keys = materials.encrypted_data_keys - return self.keyring.onDecrypt(materials, encrypted_data_keys) + return self.keyring.on_decrypt(materials, encrypted_data_keys) diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index fe32425b..1f1e5e83 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -3,6 +3,7 @@ import abc + from attrs import define from ..exceptions import S3EncryptionClientError @@ -18,11 +19,11 @@ class AbstractKeyring(abc.ABC): # enableLegacyWrappingAlgorithms: bool = field(default=False) @abc.abstractmethod - def onEncrypt(self, encMaterials): + def on_encrypt(self, enc_materials): """Process encryption materials. Args: - encMaterials (EncryptionMaterials): Encryption materials to process + enc_materials (EncryptionMaterials): Encryption materials to process Returns: EncryptionMaterials: The processed encryption materials @@ -30,15 +31,15 @@ def onEncrypt(self, encMaterials): pass @abc.abstractmethod - def onDecrypt(self, decMaterials, encrypted_data_keys=None): - """Decrypt one of the encrypted data keys and update decMaterials. + def on_decrypt(self, dec_materials, encrypted_data_keys=None): + """Decrypt one of the encrypted data keys and update dec_materials. Args: - decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials + dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. Returns: - DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) + DecryptionMaterials: The updated dec_materials with the plaintext data key (PDK) """ pass @@ -50,52 +51,52 @@ class S3Keyring(AbstractKeyring): # Ideally this would be set, but attrs doesn't play nice # enable_legacy_wrapping_algorithms: bool = field(default=False) - def onEncrypt(self, encMaterials): + def on_encrypt(self, enc_materials): """Validate encryption materials before encryption. Args: - encMaterials (EncryptionMaterials or dict): Encryption materials + enc_materials (EncryptionMaterials or dict): Encryption materials Returns: EncryptionMaterials: The validated encryption materials """ # Convert dict to EncryptionMaterials if needed - if isinstance(encMaterials, dict): - encMaterials = EncryptionMaterials.from_dict(encMaterials) + if isinstance(enc_materials, dict): + enc_materials = EncryptionMaterials.from_dict(enc_materials) # Validate encryption materials - if not isinstance(encMaterials, EncryptionMaterials): + if not isinstance(enc_materials, EncryptionMaterials): raise S3EncryptionClientError( "Encryption materials must be an EncryptionMaterials instance or a dictionary" ) # Ensure encryption_context is a dictionary - if not isinstance(encMaterials.encryption_context, dict): + if not isinstance(enc_materials.encryption_context, dict): raise S3EncryptionClientError("Encryption context must be a dictionary") - return encMaterials + return enc_materials - def onDecrypt(self, decMaterials, encrypted_data_keys=None): + def on_decrypt(self, dec_materials, encrypted_data_keys=None): """Validate decryption materials before decryption. Args: - decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials + dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. Returns: DecryptionMaterials: The validated decryption materials """ # Validate decryption materials - if not isinstance(decMaterials, DecryptionMaterials): + if not isinstance(dec_materials, DecryptionMaterials): raise S3EncryptionClientError( "Decryption materials must be a DecryptionMaterials instance" ) - # Use encrypted_data_keys from parameters if provided, otherwise use from decMaterials + # Use encrypted_data_keys from parameters if provided, otherwise use from dec_materials edks = ( encrypted_data_keys if encrypted_data_keys is not None - else decMaterials.encrypted_data_keys + else dec_materials.encrypted_data_keys ) # Validate encrypted_data_keys @@ -103,10 +104,10 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): raise S3EncryptionClientError("No encrypted data keys provided") # Ensure encryption contexts are dictionaries - if not isinstance(decMaterials.encryption_context_from_request, dict): + if not isinstance(dec_materials.encryption_context_from_request, dict): raise S3EncryptionClientError("Encryption context from request must be a dictionary") - if not isinstance(decMaterials.encryption_context_stored, dict): + if not isinstance(dec_materials.encryption_context_stored, dict): raise S3EncryptionClientError("Stored encryption context must be a dictionary") - return decMaterials + return dec_materials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 9559ff29..e7f5804e 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -17,21 +17,21 @@ class KmsKeyring(S3Keyring): kms_key_id: str = field() enable_legacy_wrapping_algorithms: bool = field(default=False) - def onEncrypt(self, encMaterials): + def on_encrypt(self, enc_materials): """Process encryption materials using KMS. Args: - encMaterials (EncryptionMaterials): Encryption materials to process + enc_materials (EncryptionMaterials): Encryption materials to process Returns: EncryptionMaterials: The processed encryption materials with KMS-generated keys """ try: # Call parent class validation - encMaterials = super().onEncrypt(encMaterials) + enc_materials = super().on_encrypt(enc_materials) # Add default encryption context - encryption_context = encMaterials.encryption_context + encryption_context = enc_materials.encryption_context encryption_context["aws:x-amz-cek-alg"] = "AES/GCM/NoPadding" response = self.kms_client.generate_data_key( @@ -43,31 +43,31 @@ def onEncrypt(self, encMaterials): key_provider_info="kms+context", encrypted_data_key=response["CiphertextBlob"], ) - encMaterials.encrypted_data_key = encrypted_data_key - encMaterials.plaintext_data_key = response["Plaintext"] - return encMaterials + enc_materials.encrypted_data_key = encrypted_data_key + enc_materials.plaintext_data_key = response["Plaintext"] + return enc_materials except Exception: raise - def onDecrypt(self, decMaterials, encrypted_data_keys=None): - """Decrypt one of the encrypted data keys and update decMaterials. + def on_decrypt(self, dec_materials, encrypted_data_keys=None): + """Decrypt one of the encrypted data keys and update dec_materials. Args: - decMaterials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials + dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. Returns: - DecryptionMaterials: The updated decMaterials with the plaintext data key (PDK) + DecryptionMaterials: The updated dec_materials with the plaintext data key (PDK) """ try: # Call parent class validation - decMaterials = super().onDecrypt(decMaterials, encrypted_data_keys) + dec_materials = super().on_decrypt(dec_materials, encrypted_data_keys) - # Use encrypted_data_keys from parameters if provided, otherwise use from decMaterials + # Use encrypted_data_keys from parameters if provided, otherwise use from dec_materials edks = ( encrypted_data_keys if encrypted_data_keys is not None - else decMaterials.encrypted_data_keys + else dec_materials.encrypted_data_keys ) # Try to decrypt each EDK until one succeeds @@ -78,9 +78,9 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): edk_bytes = edk.encrypted_data_key if edk.key_provider_info == "kms+context": encryption_context_from_request = ( - decMaterials.encryption_context_from_request + dec_materials.encryption_context_from_request ) - encryption_context_stored = decMaterials.encryption_context_stored + encryption_context_stored = dec_materials.encryption_context_stored # Default EC MUST NOT be passed in via request if KMS_CONTEXT_DEFAULT_KEY in encryption_context_from_request: @@ -112,10 +112,10 @@ def onDecrypt(self, decMaterials, encrypted_data_keys=None): response = self.kms_client.decrypt( KeyId=self.kms_key_id, CiphertextBlob=edk_bytes, - EncryptionContext=decMaterials.encryption_context_stored, + EncryptionContext=dec_materials.encryption_context_stored, ) - decMaterials.plaintext_data_key = response["Plaintext"] - return decMaterials + dec_materials.plaintext_data_key = response["Plaintext"] + return dec_materials except Exception as e: last_exception = e continue diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index 44aa4dfa..6ce08661 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -39,7 +39,7 @@ def encrypt(self, plaintext, encryption_context=None): ) # Get encryption materials from the crypto materials manager - enc_mats = self.cmm.getEncryptionMaterials(enc_mats_request) + enc_mats = self.cmm.get_encryption_materials(enc_mats_request) # Generate initialization vector iv = os.urandom(12) @@ -137,7 +137,7 @@ def decrypt(self, response, encryption_context={}): ) # Get decryption materials from the crypto materials manager - dec_materials = self.cmm.decryptMaterials(dec_materials) + dec_materials = self.cmm.decrypt_materials(dec_materials) aesgcm = AESGCM(dec_materials.plaintext_data_key) diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py index 7e697e92..7cdbd0d4 100644 --- a/test/test_decryption_materials_integration.py +++ b/test/test_decryption_materials_integration.py @@ -11,8 +11,8 @@ class TestDecryptionMaterialsIntegration(unittest.TestCase): - def test_keyring_onDecrypt(self): - """Test that S3Keyring.onDecrypt properly handles DecryptionMaterials.""" + def test_keyring_on_decrypt(self): + """Test that S3Keyring.on_decrypt properly handles DecryptionMaterials.""" # Create a keyring keyring = S3Keyring() @@ -32,9 +32,9 @@ def test_keyring_onDecrypt(self): ) # Mock the validation method to return the materials - with patch.object(S3Keyring, "onDecrypt", return_value=materials) as mock_onDecrypt: - # Call onDecrypt - result = keyring.onDecrypt(materials, [edk]) + with patch.object(S3Keyring, "on_decrypt", return_value=materials) as mock_on_decrypt: + # Call on_decrypt + result = keyring.on_decrypt(materials, [edk]) # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) @@ -43,8 +43,8 @@ def test_keyring_onDecrypt(self): self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - def test_keyring_onDecrypt_default_EC(self): - """Test that S3Keyring.onDecrypt properly handles DecryptionMaterials.""" + def test_keyring_on_decrypt_default_EC(self): + """Test that S3Keyring.on_decrypt properly handles DecryptionMaterials.""" # Create a keyring keyring = S3Keyring() @@ -64,9 +64,9 @@ def test_keyring_onDecrypt_default_EC(self): ) # Mock the validation method to return the materials - with patch.object(S3Keyring, "onDecrypt", return_value=materials) as mock_onDecrypt: - # Call onDecrypt - result = keyring.onDecrypt(materials, [edk]) + with patch.object(S3Keyring, "on_decrypt", return_value=materials) as mock_on_decrypt: + # Call on_decrypt + result = keyring.on_decrypt(materials, [edk]) # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) @@ -75,8 +75,8 @@ def test_keyring_onDecrypt_default_EC(self): self.assertEqual(result.encryption_context_stored, {}) self.assertEqual(result.encryption_context_from_request, {}) - def test_cmm_decryptMaterials_with_dict(self): - """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles dictionary input.""" + def test_cmm_decrypt_materials_with_dict(self): + """Test that DefaultCryptoMaterialsManager.decrypt_materials properly handles dictionary input.""" # Create a mock keyring keyring = MagicMock() edk = EncryptedDataKey( @@ -84,7 +84,7 @@ def test_cmm_decryptMaterials_with_dict(self): key_provider_info="kms+context", encrypted_data_key=b"encrypted-data-key", ) - keyring.onDecrypt.return_value = DecryptionMaterials( + keyring.on_decrypt.return_value = DecryptionMaterials( iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, @@ -95,8 +95,8 @@ def test_cmm_decryptMaterials_with_dict(self): # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - # Call decryptMaterials with a dictionary - result = cmm.decryptMaterials( + # Call decrypt_materials with a dictionary + result = cmm.decrypt_materials( { "iv": b"initialization-vector", "encrypted_data_keys": [edk], @@ -113,8 +113,8 @@ def test_cmm_decryptMaterials_with_dict(self): self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) self.assertEqual(result.plaintext_data_key, b"plaintext-data-key") - def test_cmm_decryptMaterials_with_materials(self): - """Test that DefaultCryptoMaterialsManager.decryptMaterials properly handles DecryptionMaterials input.""" + def test_cmm_decrypt_materials_with_materials(self): + """Test that DefaultCryptoMaterialsManager.decrypt_materials properly handles DecryptionMaterials input.""" # Create a mock keyring keyring = MagicMock() edk = EncryptedDataKey( @@ -122,7 +122,7 @@ def test_cmm_decryptMaterials_with_materials(self): key_provider_info="kms+context", encrypted_data_key=b"encrypted-data-key", ) - keyring.onDecrypt.return_value = DecryptionMaterials( + keyring.on_decrypt.return_value = DecryptionMaterials( iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, @@ -133,14 +133,14 @@ def test_cmm_decryptMaterials_with_materials(self): # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - # Call decryptMaterials with a DecryptionMaterials instance + # Call decrypt_materials with a DecryptionMaterials instance materials = DecryptionMaterials( iv=b"initialization-vector", encrypted_data_keys=[edk], encryption_context_stored={"key1": "value1"}, encryption_context_from_request={"key2": "value2"}, ) - result = cmm.decryptMaterials(materials) + result = cmm.decrypt_materials(materials) # Verify the result is a DecryptionMaterials instance self.assertIsInstance(result, DecryptionMaterials) diff --git a/test/test_encryption_materials_integration.py b/test/test_encryption_materials_integration.py index be16662d..28356fa8 100644 --- a/test/test_encryption_materials_integration.py +++ b/test/test_encryption_materials_integration.py @@ -11,26 +11,26 @@ class TestEncryptionMaterialsIntegration(unittest.TestCase): - def test_keyring_onEncrypt(self): - """Test that S3Keyring.onEncrypt properly handles EncryptionMaterials.""" + def test_keyring_on_encrypt(self): + """Test that S3Keyring.on_encrypt properly handles EncryptionMaterials.""" # Create a keyring keyring = S3Keyring() # Create encryption materials materials = EncryptionMaterials(encryption_context={"key1": "value1"}) - # Call onEncrypt - result = keyring.onEncrypt(materials) + # Call on_encrypt + result = keyring.on_encrypt(materials) # Verify the result is an EncryptionMaterials instance self.assertIsInstance(result, EncryptionMaterials) self.assertEqual(result.encryption_context, {"key1": "value1"}) - def test_cmm_getEncryptionMaterials_with_dict(self): - """Test that DefaultCryptoMaterialsManager.getEncryptionMaterials properly handles dictionary input.""" + def test_cmm_get_encryption_materials_with_dict(self): + """Test that DefaultCryptoMaterialsManager.get_encryption_materials properly handles dictionary input.""" # Create a mock keyring keyring = MagicMock() - keyring.onEncrypt.return_value = EncryptionMaterials( + keyring.on_encrypt.return_value = EncryptionMaterials( encryption_context={"key1": "value1"}, encrypted_data_key=EncryptedDataKey( key_provider_id=b"S3Keyring", @@ -43,8 +43,8 @@ def test_cmm_getEncryptionMaterials_with_dict(self): # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - # Call getEncryptionMaterials with a dictionary - result = cmm.getEncryptionMaterials({"encryption_context": {"key1": "value1"}}) + # Call get_encryption_materials with a dictionary + result = cmm.get_encryption_materials({"encryption_context": {"key1": "value1"}}) # Verify the result is an EncryptionMaterials instance self.assertIsInstance(result, EncryptionMaterials) @@ -52,11 +52,11 @@ def test_cmm_getEncryptionMaterials_with_dict(self): self.assertIsNotNone(result.encrypted_data_key) self.assertIsNotNone(result.plaintext_data_key) - def test_cmm_getEncryptionMaterials_with_materials(self): - """Test that DefaultCryptoMaterialsManager.getEncryptionMaterials properly handles EncryptionMaterials input.""" + def test_cmm_get_encryption_materials_with_materials(self): + """Test that DefaultCryptoMaterialsManager.get_encryption_materials properly handles EncryptionMaterials input.""" # Create a mock keyring keyring = MagicMock() - keyring.onEncrypt.return_value = EncryptionMaterials( + keyring.on_encrypt.return_value = EncryptionMaterials( encryption_context={"key1": "value1"}, encrypted_data_key=EncryptedDataKey( key_provider_id=b"S3Keyring", @@ -69,9 +69,9 @@ def test_cmm_getEncryptionMaterials_with_materials(self): # Create a CMM cmm = DefaultCryptoMaterialsManager(keyring=keyring) - # Call getEncryptionMaterials with an EncryptionMaterials instance + # Call get_encryption_materials with an EncryptionMaterials instance materials = EncryptionMaterials(encryption_context={"key1": "value1"}) - result = cmm.getEncryptionMaterials(materials) + result = cmm.get_encryption_materials(materials) # Verify the result is an EncryptionMaterials instance self.assertIsInstance(result, EncryptionMaterials) From cac76ce1a15988ef09ed4900ce74cd6f7729b3b5 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 15:07:18 -0700 Subject: [PATCH 25/36] format --- src/s3_encryption/materials/kms_keyring.py | 3 +-- test/integration/test_i_s3_encryption.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index e7f5804e..e5da0593 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -123,7 +123,6 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): # If we get here, none of the EDKs could be decrypted if last_exception: raise last_exception - else: - raise S3EncryptionClientError("Failed to decrypt any of the encrypted data keys") + raise S3EncryptionClientError("Failed to decrypt any of the encrypted data keys") except Exception: raise diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index f2c4bcf4..14692c41 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -39,5 +39,4 @@ def test_simple_roundtrip(): print("Output:") print(output) raise RuntimeError - else: - print("Success!") + print("Success!") From f231ab2e6b3eb8569a14b3dfa5798eca3ad2e9ec Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 15:10:07 -0700 Subject: [PATCH 26/36] remove lock file, format, simplify attrs --- .gitignore | 4 + poetry.lock | 656 ------------------ .../materials/encrypted_data_key.py | 8 +- src/s3_encryption/materials/kms_keyring.py | 3 +- test/test_decryption_materials_integration.py | 2 +- 5 files changed, 11 insertions(+), 662 deletions(-) delete mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore index 4b1c0c91..22b9a5f7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ __pycache__/ dist/ build/ *.egg-info/ + +# Uv +.uv/ +uv.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index a89805ee..00000000 --- a/poetry.lock +++ /dev/null @@ -1,656 +0,0 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "aws-cryptographic-material-providers" -version = "1.11.0" -description = "AWS Cryptographic Material Providers Library for Python" -optional = false -python-versions = "<4.0.0,>=3.11.0" -files = [ - {file = "aws_cryptographic_material_providers-1.11.0-py3-none-any.whl", hash = "sha256:9a9f0dca5b1902a4f16fb91cc1010dee74a721f84f411e81ffb4481fc0dd095f"}, - {file = "aws_cryptographic_material_providers-1.11.0.tar.gz", hash = "sha256:4ea5f9e5cc003e97d2ef98079dc25d8c49a0db01315ee887d19fd2f1c85ae9c3"}, -] - -[package.dependencies] -aws-cryptography-internal-dynamodb = "1.11.0" -aws-cryptography-internal-kms = "1.11.0" -aws-cryptography-internal-primitives = "1.11.0" -aws-cryptography-internal-standard-library = "1.11.0" - -[[package]] -name = "aws-cryptography-internal-dynamodb" -version = "1.11.0" -description = "" -optional = false -python-versions = "<4.0.0,>=3.11.0" -files = [ - {file = "aws_cryptography_internal_dynamodb-1.11.0-py3-none-any.whl", hash = "sha256:5a2da0ae6829d725f24018d001f4c733605f213820b723b6c75015843dc2427c"}, - {file = "aws_cryptography_internal_dynamodb-1.11.0.tar.gz", hash = "sha256:0800921ebb5dafc2853a2f5449f74aa03d24acd9ddb2ee58edca4002b97a5da5"}, -] - -[package.dependencies] -aws-cryptography-internal-standard-library = "1.11.0" -boto3 = ">=1.35.42,<2.0.0" - -[[package]] -name = "aws-cryptography-internal-kms" -version = "1.11.0" -description = "" -optional = false -python-versions = "<4.0.0,>=3.11.0" -files = [ - {file = "aws_cryptography_internal_kms-1.11.0-py3-none-any.whl", hash = "sha256:1c23cc8e970252fc7627868fc6b7a002400ec1d555ac29368e0eaddcceb07953"}, - {file = "aws_cryptography_internal_kms-1.11.0.tar.gz", hash = "sha256:a3ff5105b3e1c9d81e9698e0efc80de8a6bb8078b4512f9b39ed0f6161aae172"}, -] - -[package.dependencies] -aws-cryptography-internal-standard-library = "1.11.0" -boto3 = ">=1.35.42,<2.0.0" - -[[package]] -name = "aws-cryptography-internal-primitives" -version = "1.11.0" -description = "" -optional = false -python-versions = "<4.0.0,>=3.11.0" -files = [ - {file = "aws_cryptography_internal_primitives-1.11.0-py3-none-any.whl", hash = "sha256:84200885113f3534f4bff819ac1603c6d5c3bdd4d5c83a1b73ac2462cecec49b"}, - {file = "aws_cryptography_internal_primitives-1.11.0.tar.gz", hash = "sha256:9072af2c403b9e729dc767b44d1d642fa924a317a5bdbdffdf6dba0e93dc7996"}, -] - -[package.dependencies] -aws-cryptography-internal-standard-library = "1.11.0" -cryptography = ">=43.0.1,<46" - -[[package]] -name = "aws-cryptography-internal-standard-library" -version = "1.11.0" -description = "" -optional = false -python-versions = "<4.0.0,>=3.11.0" -files = [ - {file = "aws_cryptography_internal_standard_library-1.11.0-py3-none-any.whl", hash = "sha256:a2d5a4d8f70bce7242e8ebe06742223b8cd93253ed8081f44d7a8c1a086871e1"}, - {file = "aws_cryptography_internal_standard_library-1.11.0.tar.gz", hash = "sha256:36d82c6bc0361cf0ec3b7181804d375718f5c297949ddd902670f4452ecad3b0"}, -] - -[package.dependencies] -DafnyRuntimePython = "4.9.0" -pytz = ">=2023.3.post1,<2025.0.0" - -[[package]] -name = "black" -version = "24.10.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "boto3" -version = "1.39.14" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "boto3-1.39.14-py3-none-any.whl", hash = "sha256:82c6868cad18c3bd4170915e9525f9af5f83e9779c528417f8863629558fc2d0"}, - {file = "boto3-1.39.14.tar.gz", hash = "sha256:fabb16360a93b449d5241006485bcc761c26694e75ac01009f4459f114acc06e"}, -] - -[package.dependencies] -botocore = ">=1.39.14,<1.40.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.13.0,<0.14.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.39.14" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.9" -files = [ - {file = "botocore-1.39.14-py3-none-any.whl", hash = "sha256:4ed551c77194167b7e8063f33059bc2f9b2ead0ed4ee33dc7857273648ed4349"}, - {file = "botocore-1.39.14.tar.gz", hash = "sha256:7fc44d4ad13b524e5d8a6296785776ef5898ac026ff74df9b35313831d507926"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.23.8)"] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "click" -version = "8.2.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "cryptography" -version = "43.0.3" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "dafnyruntimepython" -version = "4.9.0" -description = "Dafny runtime for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "DafnyRuntimePython-4.9.0-py3-none-any.whl", hash = "sha256:c9cdcf127f5b6a4c6c9cf69016b9486318c3a6600e7f03fcbc621f6a5398479c"}, - {file = "dafnyruntimepython-4.9.0.tar.gz", hash = "sha256:03a4c2dbbe45c13dc2c7dbefad01812367b3bb217a14b4b848d7e94ef5c08cee"}, -] - -[[package]] -name = "flake8" -version = "7.3.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.9" -files = [ - {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, - {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.14.0,<2.15.0" -pyflakes = ">=3.4.0,<3.5.0" - -[[package]] -name = "flake8-docstrings" -version = "1.7.0" -description = "Extension for flake8 which uses pydocstyle to check docstrings" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, - {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, -] - -[package.dependencies] -flake8 = ">=3" -pydocstyle = ">=2.1" - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, - {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, -] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - -[[package]] -name = "pyflakes" -version = "3.4.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, - {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "8.4.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2024.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, -] - -[[package]] -name = "s3transfer" -version = "0.13.1" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.9" -files = [ - {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"}, - {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"}, -] - -[package.dependencies] -botocore = ">=1.37.4,<2.0a.0" - -[package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "e0d80bd0119ad8c72dfd80afa69019310ce4c75a9f81e6da9bb80666657840e2" diff --git a/src/s3_encryption/materials/encrypted_data_key.py b/src/s3_encryption/materials/encrypted_data_key.py index 51d47c3b..057fafda 100644 --- a/src/s3_encryption/materials/encrypted_data_key.py +++ b/src/s3_encryption/materials/encrypted_data_key.py @@ -1,6 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from attrs import define, field +from attrs import define @define @@ -16,6 +16,6 @@ class EncryptedDataKey: encrypted_data_key (bytes): The encrypted data key """ - key_provider_info: str = field() - key_provider_id: bytes = field() - encrypted_data_key: bytes = field() + key_provider_info: str + key_provider_id: bytes + encrypted_data_key: bytes diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index e5da0593..27633743 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from attrs import define, field +from botocore import client from ..exceptions import S3EncryptionClientError from .encrypted_data_key import EncryptedDataKey @@ -13,7 +14,7 @@ @define class KmsKeyring(S3Keyring): - kms_client = field() + kms_client = client.BaseClient kms_key_id: str = field() enable_legacy_wrapping_algorithms: bool = field(default=False) diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py index 7cdbd0d4..23e65c22 100644 --- a/test/test_decryption_materials_integration.py +++ b/test/test_decryption_materials_integration.py @@ -43,7 +43,7 @@ def test_keyring_on_decrypt(self): self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - def test_keyring_on_decrypt_default_EC(self): + def test_keyring_on_decrypt_default_enc_ctx(self): """Test that S3Keyring.on_decrypt properly handles DecryptionMaterials.""" # Create a keyring keyring = S3Keyring() From 2d686bc77c4a57ad76af5f93624a5cdf98c9a3bc Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 15:12:24 -0700 Subject: [PATCH 27/36] remove typehint --- src/s3_encryption/materials/kms_keyring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 27633743..8b6914da 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -14,7 +14,7 @@ @define class KmsKeyring(S3Keyring): - kms_client = client.BaseClient + kms_client = field() kms_key_id: str = field() enable_legacy_wrapping_algorithms: bool = field(default=False) From aed461ecd07d796507367c947c32d67b3e88bd03 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 15:21:23 -0700 Subject: [PATCH 28/36] use pytest --- test/test_decryption_materials.py | 48 ++++++++--------- test/test_decryption_materials_integration.py | 52 +++++++++---------- test/test_encryption_materials.py | 28 +++++----- test/test_encryption_materials_integration.py | 28 +++++----- test/test_metadata.py | 42 +++++++-------- 5 files changed, 89 insertions(+), 109 deletions(-) diff --git a/test/test_decryption_materials.py b/test/test_decryption_materials.py index 2ada8af8..94cd0c4e 100644 --- a/test/test_decryption_materials.py +++ b/test/test_decryption_materials.py @@ -1,21 +1,21 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import unittest +import pytest from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey from src.s3_encryption.materials.materials import DecryptionMaterials -class TestDecryptionMaterials(unittest.TestCase): +class TestDecryptionMaterials: def test_create_decryption_materials(self): """Test creating a DecryptionMaterials instance.""" materials = DecryptionMaterials() - self.assertEqual(materials.encrypted_data_keys, []) - self.assertEqual(materials.encryption_context_stored, {}) - self.assertEqual(materials.encryption_context_from_request, {}) - self.assertIsNone(materials.iv) - self.assertIsNone(materials.plaintext_data_key) + assert materials.encrypted_data_keys == [] + assert materials.encryption_context_stored == {} + assert materials.encryption_context_from_request == {} + assert materials.iv is None + assert materials.plaintext_data_key is None def test_create_with_parameters(self): """Test creating a DecryptionMaterials instance with parameters.""" @@ -39,11 +39,11 @@ def test_create_with_parameters(self): plaintext_data_key=plaintext_data_key, ) - self.assertEqual(materials.iv, iv) - self.assertEqual(materials.encrypted_data_keys, encrypted_data_keys) - self.assertEqual(materials.encryption_context_stored, encryption_context_stored) - self.assertEqual(materials.encryption_context_from_request, encryption_context_from_request) - self.assertEqual(materials.plaintext_data_key, plaintext_data_key) + assert materials.iv == iv + assert materials.encrypted_data_keys == encrypted_data_keys + assert materials.encryption_context_stored == encryption_context_stored + assert materials.encryption_context_from_request == encryption_context_from_request + assert materials.plaintext_data_key == plaintext_data_key def test_from_dict(self): """Test creating a DecryptionMaterials instance from a dictionary.""" @@ -60,11 +60,11 @@ def test_from_dict(self): "PDK": b"plaintext-data-key", } materials = DecryptionMaterials.from_dict(materials_dict) - self.assertEqual(materials.iv, b"initialization-vector") - self.assertEqual(materials.encrypted_data_keys, [edk]) - self.assertEqual(materials.encryption_context_stored, {"key1": "value1"}) - self.assertEqual(materials.encryption_context_from_request, {"key2": "value2"}) - self.assertEqual(materials.plaintext_data_key, b"plaintext-data-key") + assert materials.iv == b"initialization-vector" + assert materials.encrypted_data_keys == [edk] + assert materials.encryption_context_stored == {"key1": "value1"} + assert materials.encryption_context_from_request == {"key2": "value2"} + assert materials.plaintext_data_key == b"plaintext-data-key" def test_to_dict(self): """Test converting a DecryptionMaterials instance to a dictionary.""" @@ -81,12 +81,8 @@ def test_to_dict(self): plaintext_data_key=b"plaintext-data-key", ) materials_dict = materials.to_dict() - self.assertEqual(materials_dict["iv"], b"initialization-vector") - self.assertEqual(materials_dict["encrypted_data_keys"], [edk]) - self.assertEqual(materials_dict["encryption_context_stored"], {"key1": "value1"}) - self.assertEqual(materials_dict["encryption_context_from_request"], {"key2": "value2"}) - self.assertEqual(materials_dict["PDK"], b"plaintext-data-key") - - -if __name__ == "__main__": - unittest.main() + assert materials_dict["iv"] == b"initialization-vector" + assert materials_dict["encrypted_data_keys"] == [edk] + assert materials_dict["encryption_context_stored"] == {"key1": "value1"} + assert materials_dict["encryption_context_from_request"] == {"key2": "value2"} + assert materials_dict["PDK"] == b"plaintext-data-key" diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py index 23e65c22..7a20e163 100644 --- a/test/test_decryption_materials_integration.py +++ b/test/test_decryption_materials_integration.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import unittest +import pytest from unittest.mock import MagicMock, patch from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager @@ -10,7 +10,7 @@ from src.s3_encryption.materials.materials import DecryptionMaterials -class TestDecryptionMaterialsIntegration(unittest.TestCase): +class TestDecryptionMaterialsIntegration: def test_keyring_on_decrypt(self): """Test that S3Keyring.on_decrypt properly handles DecryptionMaterials.""" # Create a keyring @@ -37,11 +37,11 @@ def test_keyring_on_decrypt(self): result = keyring.on_decrypt(materials, [edk]) # Verify the result is a DecryptionMaterials instance - self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b"initialization-vector") - self.assertEqual(result.encrypted_data_keys, [edk]) - self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) - self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) + assert isinstance(result, DecryptionMaterials) + assert result.iv == b"initialization-vector" + assert result.encrypted_data_keys == [edk] + assert result.encryption_context_stored == {"key1": "value1"} + assert result.encryption_context_from_request == {"key2": "value2"} def test_keyring_on_decrypt_default_enc_ctx(self): """Test that S3Keyring.on_decrypt properly handles DecryptionMaterials.""" @@ -69,11 +69,11 @@ def test_keyring_on_decrypt_default_enc_ctx(self): result = keyring.on_decrypt(materials, [edk]) # Verify the result is a DecryptionMaterials instance - self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b"initialization-vector") - self.assertEqual(result.encrypted_data_keys, [edk]) - self.assertEqual(result.encryption_context_stored, {}) - self.assertEqual(result.encryption_context_from_request, {}) + assert isinstance(result, DecryptionMaterials) + assert result.iv == b"initialization-vector" + assert result.encrypted_data_keys == [edk] + assert result.encryption_context_stored == {} + assert result.encryption_context_from_request == {} def test_cmm_decrypt_materials_with_dict(self): """Test that DefaultCryptoMaterialsManager.decrypt_materials properly handles dictionary input.""" @@ -106,12 +106,12 @@ def test_cmm_decrypt_materials_with_dict(self): ) # Verify the result is a DecryptionMaterials instance - self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b"initialization-vector") - self.assertEqual(result.encrypted_data_keys, [edk]) - self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) - self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - self.assertEqual(result.plaintext_data_key, b"plaintext-data-key") + assert isinstance(result, DecryptionMaterials) + assert result.iv == b"initialization-vector" + assert result.encrypted_data_keys == [edk] + assert result.encryption_context_stored == {"key1": "value1"} + assert result.encryption_context_from_request == {"key2": "value2"} + assert result.plaintext_data_key == b"plaintext-data-key" def test_cmm_decrypt_materials_with_materials(self): """Test that DefaultCryptoMaterialsManager.decrypt_materials properly handles DecryptionMaterials input.""" @@ -143,13 +143,9 @@ def test_cmm_decrypt_materials_with_materials(self): result = cmm.decrypt_materials(materials) # Verify the result is a DecryptionMaterials instance - self.assertIsInstance(result, DecryptionMaterials) - self.assertEqual(result.iv, b"initialization-vector") - self.assertEqual(result.encrypted_data_keys, [edk]) - self.assertEqual(result.encryption_context_stored, {"key1": "value1"}) - self.assertEqual(result.encryption_context_from_request, {"key2": "value2"}) - self.assertEqual(result.plaintext_data_key, b"plaintext-data-key") - - -if __name__ == "__main__": - unittest.main() + assert isinstance(result, DecryptionMaterials) + assert result.iv == b"initialization-vector" + assert result.encrypted_data_keys == [edk] + assert result.encryption_context_stored == {"key1": "value1"} + assert result.encryption_context_from_request == {"key2": "value2"} + assert result.plaintext_data_key == b"plaintext-data-key" diff --git a/test/test_encryption_materials.py b/test/test_encryption_materials.py index ffa5a449..bd180d59 100644 --- a/test/test_encryption_materials.py +++ b/test/test_encryption_materials.py @@ -1,25 +1,25 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import unittest +import pytest from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey from src.s3_encryption.materials.materials import EncryptionMaterials -class TestEncryptionMaterials(unittest.TestCase): +class TestEncryptionMaterials: def test_create_encryption_materials(self): """Test creating an EncryptionMaterials instance.""" materials = EncryptionMaterials() - self.assertEqual(materials.encryption_context, {}) - self.assertIsNone(materials.encrypted_data_key) - self.assertIsNone(materials.plaintext_data_key) + assert materials.encryption_context == {} + assert materials.encrypted_data_key is None + assert materials.plaintext_data_key is None def test_create_with_encryption_context(self): """Test creating an EncryptionMaterials instance with an encryption context.""" encryption_context = {"key1": "value1", "key2": "value2"} materials = EncryptionMaterials(encryption_context=encryption_context) - self.assertEqual(materials.encryption_context, encryption_context) + assert materials.encryption_context == encryption_context def test_from_dict(self): """Test creating an EncryptionMaterials instance from a dictionary.""" @@ -34,9 +34,9 @@ def test_from_dict(self): "PDK": b"plaintext-data-key", } materials = EncryptionMaterials.from_dict(materials_dict) - self.assertEqual(materials.encryption_context, {"key1": "value1"}) - self.assertEqual(materials.encrypted_data_key, edk) - self.assertEqual(materials.plaintext_data_key, b"plaintext-data-key") + assert materials.encryption_context == {"key1": "value1"} + assert materials.encrypted_data_key == edk + assert materials.plaintext_data_key == b"plaintext-data-key" def test_to_dict(self): """Test converting an EncryptionMaterials instance to a dictionary.""" @@ -51,10 +51,6 @@ def test_to_dict(self): plaintext_data_key=b"plaintext-data-key", ) materials_dict = materials.to_dict() - self.assertEqual(materials_dict["encryption_context"], {"key1": "value1"}) - self.assertEqual(materials_dict["encrypted_data_key"], edk) - self.assertEqual(materials_dict["PDK"], b"plaintext-data-key") - - -if __name__ == "__main__": - unittest.main() + assert materials_dict["encryption_context"] == {"key1": "value1"} + assert materials_dict["encrypted_data_key"] == edk + assert materials_dict["PDK"] == b"plaintext-data-key" diff --git a/test/test_encryption_materials_integration.py b/test/test_encryption_materials_integration.py index 28356fa8..abac6f8c 100644 --- a/test/test_encryption_materials_integration.py +++ b/test/test_encryption_materials_integration.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import unittest +import pytest from unittest.mock import MagicMock from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager @@ -10,7 +10,7 @@ from src.s3_encryption.materials.materials import EncryptionMaterials -class TestEncryptionMaterialsIntegration(unittest.TestCase): +class TestEncryptionMaterialsIntegration: def test_keyring_on_encrypt(self): """Test that S3Keyring.on_encrypt properly handles EncryptionMaterials.""" # Create a keyring @@ -23,8 +23,8 @@ def test_keyring_on_encrypt(self): result = keyring.on_encrypt(materials) # Verify the result is an EncryptionMaterials instance - self.assertIsInstance(result, EncryptionMaterials) - self.assertEqual(result.encryption_context, {"key1": "value1"}) + assert isinstance(result, EncryptionMaterials) + assert result.encryption_context == {"key1": "value1"} def test_cmm_get_encryption_materials_with_dict(self): """Test that DefaultCryptoMaterialsManager.get_encryption_materials properly handles dictionary input.""" @@ -47,10 +47,10 @@ def test_cmm_get_encryption_materials_with_dict(self): result = cmm.get_encryption_materials({"encryption_context": {"key1": "value1"}}) # Verify the result is an EncryptionMaterials instance - self.assertIsInstance(result, EncryptionMaterials) - self.assertEqual(result.encryption_context, {"key1": "value1"}) - self.assertIsNotNone(result.encrypted_data_key) - self.assertIsNotNone(result.plaintext_data_key) + assert isinstance(result, EncryptionMaterials) + assert result.encryption_context == {"key1": "value1"} + assert result.encrypted_data_key is not None + assert result.plaintext_data_key is not None def test_cmm_get_encryption_materials_with_materials(self): """Test that DefaultCryptoMaterialsManager.get_encryption_materials properly handles EncryptionMaterials input.""" @@ -74,11 +74,7 @@ def test_cmm_get_encryption_materials_with_materials(self): result = cmm.get_encryption_materials(materials) # Verify the result is an EncryptionMaterials instance - self.assertIsInstance(result, EncryptionMaterials) - self.assertEqual(result.encryption_context, {"key1": "value1"}) - self.assertIsNotNone(result.encrypted_data_key) - self.assertIsNotNone(result.plaintext_data_key) - - -if __name__ == "__main__": - unittest.main() + assert isinstance(result, EncryptionMaterials) + assert result.encryption_context == {"key1": "value1"} + assert result.encrypted_data_key is not None + assert result.plaintext_data_key is not None diff --git a/test/test_metadata.py b/test/test_metadata.py index 2c26c336..2abd2866 100644 --- a/test/test_metadata.py +++ b/test/test_metadata.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import os import sys -import unittest +import pytest # Add the src directory to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) @@ -10,7 +10,7 @@ from s3_encryption.metadata import ObjectMetadata -class TestObjectMetadata(unittest.TestCase): +class TestObjectMetadata: def test_from_dict(self): # Create a metadata dictionary metadata_dict = { @@ -24,17 +24,17 @@ def test_from_dict(self): metadata = ObjectMetadata.from_dict(metadata_dict) # Verify that the fields were populated correctly - self.assertEqual(metadata.encrypted_data_key_v2, "encrypted-key-data") - self.assertEqual(metadata.encrypted_data_key_algorithm, "kms+context") - self.assertEqual(metadata.content_iv, "base64-encoded-iv") - self.assertEqual(metadata.content_cipher, "AES/GCM/NoPadding") + assert metadata.encrypted_data_key_v2 == "encrypted-key-data" + assert metadata.encrypted_data_key_algorithm == "kms+context" + assert metadata.content_iv == "base64-encoded-iv" + assert metadata.content_cipher == "AES/GCM/NoPadding" # Verify that fields not in the dictionary are None - self.assertIsNone(metadata.encrypted_data_key_v1) - self.assertIsNone(metadata.encrypted_data_key_context) + assert metadata.encrypted_data_key_v1 is None + assert metadata.encrypted_data_key_context is None # Note: content_cipher_tag_length is None because it's not in the input dictionary - self.assertIsNone(metadata.content_cipher_tag_length) - self.assertIsNone(metadata.instruction_file) + assert metadata.content_cipher_tag_length is None + assert metadata.instruction_file is None def test_to_dict(self): # Create an ObjectMetadata instance with some fields set @@ -49,17 +49,17 @@ def test_to_dict(self): metadata_dict = metadata.to_dict() # Verify that the dictionary contains the expected keys and values - self.assertEqual(metadata_dict["x-amz-key-v2"], "encrypted-key-data") - self.assertEqual(metadata_dict["x-amz-wrap-alg"], "kms+context") - self.assertEqual(metadata_dict["x-amz-iv"], "base64-encoded-iv") - self.assertEqual(metadata_dict["x-amz-cek-alg"], "AES/GCM/NoPadding") + assert metadata_dict["x-amz-key-v2"] == "encrypted-key-data" + assert metadata_dict["x-amz-wrap-alg"] == "kms+context" + assert metadata_dict["x-amz-iv"] == "base64-encoded-iv" + assert metadata_dict["x-amz-cek-alg"] == "AES/GCM/NoPadding" # Verify that fields that are None are not included in the dictionary - self.assertNotIn("x-amz-key", metadata_dict) - self.assertNotIn("x-amz-matdesc", metadata_dict) + assert "x-amz-key" not in metadata_dict + assert "x-amz-matdesc" not in metadata_dict # Note: content_cipher_tag_length has a default value of "128" - self.assertEqual(metadata_dict.get("x-amz-tag-len"), "128") - self.assertNotIn("x-amz-crypto-instr-file", metadata_dict) + assert metadata_dict.get("x-amz-tag-len") == "128" + assert "x-amz-crypto-instr-file" not in metadata_dict def test_roundtrip(self): # Create a metadata dictionary @@ -79,8 +79,4 @@ def test_roundtrip(self): result_dict.pop("x-amz-tag-len") # Verify that the result matches the original - self.assertEqual(result_dict, original_dict) - - -if __name__ == "__main__": - unittest.main() + assert result_dict == original_dict From 2f6bc1eefc2121f53e54eb8d60ddf73724cddc0e Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 11 Aug 2025 15:25:43 -0700 Subject: [PATCH 29/36] remove uv lock --- uv.lock | 369 -------------------------------------------------------- 1 file changed, 369 deletions(-) delete mode 100644 uv.lock diff --git a/uv.lock b/uv.lock deleted file mode 100644 index ebc7a7e9..00000000 --- a/uv.lock +++ /dev/null @@ -1,369 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" - -[[package]] -name = "amazon-s3-encryption-client-python" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "attrs" }, - { name = "boto3" }, - { name = "cryptography" }, -] - -[package.optional-dependencies] -dev = [ - { name = "black" }, - { name = "ruff" }, -] -test = [ - { name = "pytest" }, -] - -[package.metadata] -requires-dist = [ - { name = "attrs", specifier = ">=25.1.0" }, - { name = "black", marker = "extra == 'dev'", specifier = ">=24.3.0" }, - { name = "boto3", specifier = ">=1.37.2" }, - { name = "cryptography", specifier = ">=45.0.6" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, -] -provides-extras = ["test", "dev"] - -[[package]] -name = "attrs" -version = "25.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, -] - -[[package]] -name = "black" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, -] - -[[package]] -name = "boto3" -version = "1.40.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, - { name = "jmespath" }, - { name = "s3transfer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/5a/f31556d817e872c2723196a34b197d971d78297b22b8bae0ae6d93f7f9c1/boto3-1.40.7.tar.gz", hash = "sha256:61b15f70761f1eadd721c6ba41a92658f003eaaef09500ca7642f5ae68ec8945", size = 111989, upload-time = "2025-08-11T19:20:45.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/e3/f2a77f4809ffe4e896c2e6186db88333ae980f52a91b28e9fd068d8f5506/boto3-1.40.7-py3-none-any.whl", hash = "sha256:8727cac601a679d2885dc78b8119a0548bbbe04e49b72f7d94021a629154c080", size = 140061, upload-time = "2025-08-11T19:20:43.173Z" }, -] - -[[package]] -name = "botocore" -version = "1.40.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jmespath" }, - { name = "python-dateutil" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/d7/5e559918410b259c1e54a4646ff39c56433e1c9cefa5e66ab0f06716cee8/botocore-1.40.7.tar.gz", hash = "sha256:33793696680cf3a0c4b5ace4f9070c67c4d4fcb19c999fd85cfee55de3dcf913", size = 14318282, upload-time = "2025-08-11T19:20:33.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fa/bb7ec68b24d1b4678d341a305cbfed78a593e6383c86a70727410e4d0e11/botocore-1.40.7-py3-none-any.whl", hash = "sha256:a06956f3d7222e80ef6ae193608f358c3b7898e1a2b88553479d8f9737fbb03e", size = 13981488, upload-time = "2025-08-11T19:20:27.303Z" }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, -] - -[[package]] -name = "click" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, -] - -[[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, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "45.0.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, - { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, - { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, - { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, - { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, - { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, - { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, - { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, - { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, - { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, - { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, - { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, - { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jmespath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "ruff" -version = "0.12.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, -] - -[[package]] -name = "s3transfer" -version = "0.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] From fa000a5983c940b3df38d555bf7d680ba48dd1c7 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 09:52:30 -0700 Subject: [PATCH 30/36] ruff fixes --- src/s3_encryption/__init__.py | 37 +++++++++++++++++-- src/s3_encryption/exceptions.py | 4 ++ src/s3_encryption/materials/__init__.py | 5 +++ .../materials/crypto_materials_manager.py | 26 +++++++++++-- .../materials/encrypted_data_key.py | 5 +++ src/s3_encryption/materials/keyring.py | 29 +++++++++------ src/s3_encryption/materials/kms_keyring.py | 30 ++++++++++++--- src/s3_encryption/materials/materials.py | 6 +++ src/s3_encryption/metadata.py | 8 +++- src/s3_encryption/pipelines.py | 17 ++++++--- 10 files changed, 137 insertions(+), 30 deletions(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index 0b56454b..31ccf0a6 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -1,5 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Top-level S3 Encryption Client v3 for Python package.""" import io from attrs import define, field @@ -10,13 +11,12 @@ DefaultCryptoMaterialsManager, ) from .materials.keyring import AbstractKeyring -from .metadata import ObjectMetadata from .pipelines import GetEncryptedObjectPipeline, PutEncryptedObjectPipeline @define class S3EncryptionClientConfig: - """Configuration object for the S3 Encryption Client""" + """Configuration object for the S3 Encryption Client.""" keyring: AbstractKeyring cmm: AbstractCryptoMaterialsManager = field() @@ -28,10 +28,28 @@ def _default_cmm_for_keyring(self): @define class S3EncryptionClient: + """Client for encrypting and decrypting S3 objects. + + This client wraps a boto3 S3 client and provides encryption and decryption + capabilities for S3 objects using the configured keyring and crypto materials manager. + """ wrapped_s3_client = field() config: S3EncryptionClientConfig = field() def put_object(self, **kwargs): + """Encrypt and upload an object to S3. + + This method encrypts the provided object body before uploading it to S3. + It handles the encryption process using the configured crypto materials manager. + + Args: + **kwargs: Arguments to pass to the S3 client's put_object method. + Must include Bucket, Key, and Body parameters. + May include EncryptionContext for additional authenticated data. + + Returns: + The response from the S3 client's put_object method. + """ # Extract required parameters from kwargs bucket = kwargs.pop("Bucket") key = kwargs.pop("Key") @@ -45,7 +63,7 @@ def put_object(self, **kwargs): data_bytes = body # We probably just shouldn't support strings, use utf8 for now # TODO: look deeper into this, what does normal boto3 do? - if type(body) == str: + if isinstance(body, str): data_bytes = body.encode("utf-8") encrypted_data, encryption_metadata = pipeline.encrypt( data_bytes, encryption_context=encryption_context @@ -64,6 +82,19 @@ def put_object(self, **kwargs): return self.wrapped_s3_client.put_object(**params) def get_object(self, **kwargs): + """Download and decrypt an object from S3. + + This method downloads an encrypted object from S3 and decrypts it + using the configured crypto materials manager. + + Args: + **kwargs: Arguments to pass to the S3 client's get_object method. + May include EncryptionContext if it was used during encryption. + + Returns: + The response from the S3 client's get_object method with the Body + replaced with a StreamingBody containing the decrypted data. + """ # Extract encryption context if provided encryption_context = kwargs.pop("EncryptionContext", None) diff --git a/src/s3_encryption/exceptions.py b/src/s3_encryption/exceptions.py index 034b2bdf..d7d44df2 100644 --- a/src/s3_encryption/exceptions.py +++ b/src/s3_encryption/exceptions.py @@ -1,4 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Exceptions for the S3 Encryption Client. + +This module contains custom exception classes used throughout the S3 Encryption Client. +""" class S3EncryptionClientError(Exception): """Exception class for S3 Encryption Client errors.""" diff --git a/src/s3_encryption/materials/__init__.py b/src/s3_encryption/materials/__init__.py index b602bc91..c67d5802 100644 --- a/src/s3_encryption/materials/__init__.py +++ b/src/s3_encryption/materials/__init__.py @@ -1,5 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Materials package for S3 Encryption Client. + +This package contains classes and interfaces for cryptographic materials +management, including keyrings, crypto materials managers, and encrypted data keys. +""" from .crypto_materials_manager import AbstractCryptoMaterialsManager, DefaultCryptoMaterialsManager from .encrypted_data_key import EncryptedDataKey diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index 06965eff..3ecb2489 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -1,5 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Crypto materials manager module for S3 Encryption Client. + +This module provides interfaces and implementations for crypto materials managers, +which are responsible for coordinating the generation and use of cryptographic materials. +""" import abc @@ -11,12 +16,18 @@ # API Stub for CMM class AbstractCryptoMaterialsManager(abc.ABC): + """Abstract base class for crypto materials managers. + + A crypto materials manager is responsible for generating encryption materials + and processing decryption materials using a keyring. + """ @abc.abstractmethod def get_encryption_materials(self, enc_mats_request): """Get encryption materials from the keyring. Args: - enc_mats_request (Dict[str, Any] or EncryptionMaterials): Request containing encryption parameters + enc_mats_request (Dict[str, Any] or EncryptionMaterials): Request containing encryption + parameters Returns: EncryptionMaterials: The encryption materials @@ -28,7 +39,8 @@ def decrypt_materials(self, dec_mats_request): """Decrypt materials using the keyring. Args: - dec_mats_request (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters + dec_mats_request (Dict[str, Any] or DecryptionMaterials): Request containing decryption + parameters Returns: DecryptionMaterials: The decryption materials @@ -38,6 +50,13 @@ def decrypt_materials(self, dec_mats_request): @define class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): + """Default implementation of the crypto materials manager. + + This implementation delegates encryption and decryption operations to a single keyring. + + Attributes: + keyring (AbstractKeyring): The keyring to use for cryptographic operations + """ keyring: AbstractKeyring def get_encryption_materials(self, enc_mats_request): @@ -63,7 +82,8 @@ def decrypt_materials(self, dec_mats_request): """Decrypt materials using the keyring. Args: - dec_mats_request (Dict[str, Any] or DecryptionMaterials): Request containing decryption parameters + dec_mats_request (Dict[str, Any] or DecryptionMaterials): Request containing decryption + parameters Returns: DecryptionMaterials: The decryption materials diff --git a/src/s3_encryption/materials/encrypted_data_key.py b/src/s3_encryption/materials/encrypted_data_key.py index 057fafda..28401b40 100644 --- a/src/s3_encryption/materials/encrypted_data_key.py +++ b/src/s3_encryption/materials/encrypted_data_key.py @@ -1,5 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Encrypted data key module for S3 Encryption Client. + +This module provides the EncryptedDataKey class which represents an encrypted +data key used in the S3 encryption process. +""" from attrs import define diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 1f1e5e83..6115b02f 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -1,6 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Keyring module for S3 Encryption Client. +This module provides interfaces and implementations for keyrings, which are +responsible for encrypting and decrypting data keys used in the S3 encryption process. +""" import abc @@ -12,11 +16,11 @@ @define class AbstractKeyring(abc.ABC): - # Ideally, all keyrings would inherit this field. - # However, attrs doesn't allow us to set a default here, - # when inheriting keyrings have optional fields. - # Even without a default it doesn't seem to play nice with attrs. - # enableLegacyWrappingAlgorithms: bool = field(default=False) + """Abstract base class for keyrings. + + A keyring is responsible for encrypting and decrypting data keys. + Concrete implementations handle specific key providers like KMS. + """ @abc.abstractmethod def on_encrypt(self, enc_materials): @@ -35,8 +39,10 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): """Decrypt one of the encrypted data keys and update dec_materials. Args: - dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials - encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. + dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing + decryption materials + encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data + keys to try. Returns: DecryptionMaterials: The updated dec_materials with the plaintext data key (PDK) @@ -48,9 +54,6 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): class S3Keyring(AbstractKeyring): """Base class for S3 encryption keyrings that provides common validation logic.""" - # Ideally this would be set, but attrs doesn't play nice - # enable_legacy_wrapping_algorithms: bool = field(default=False) - def on_encrypt(self, enc_materials): """Validate encryption materials before encryption. @@ -80,8 +83,10 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): """Validate decryption materials before decryption. Args: - dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials - encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. + dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing + decryption materials + encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data + keys to try. Returns: DecryptionMaterials: The validated decryption materials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 8b6914da..303a5f93 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -1,8 +1,12 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""KMS keyring module for S3 Encryption Client. + +This module provides a KMS-based keyring implementation that uses AWS KMS +to generate and decrypt data keys for S3 object encryption. +""" from attrs import define, field -from botocore import client from ..exceptions import S3EncryptionClientError from .encrypted_data_key import EncryptedDataKey @@ -14,6 +18,15 @@ @define class KmsKeyring(S3Keyring): + """KMS implementation of the S3 keyring. + + This keyring uses AWS KMS to generate and decrypt data keys. + + Attributes: + kms_client: The boto3 KMS client + kms_key_id (str): The KMS key ID to use + enable_legacy_wrapping_algorithms (bool): Whether to enable legacy wrapping algorithms + """ kms_client = field() kms_key_id: str = field() enable_legacy_wrapping_algorithms: bool = field(default=False) @@ -54,8 +67,10 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): """Decrypt one of the encrypted data keys and update dec_materials. Args: - dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing decryption materials - encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data keys to try. + dec_materials (DecryptionMaterials): A DecryptionMaterials instance containing + decryption materials + encrypted_data_keys (List[EncryptedDataKey], optional): A list of encrypted data + keys to try. Returns: DecryptionMaterials: The updated dec_materials with the plaintext data key (PDK) @@ -86,7 +101,8 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): # Default EC MUST NOT be passed in via request if KMS_CONTEXT_DEFAULT_KEY in encryption_context_from_request: raise S3EncryptionClientError( - f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the S3 encryption client" + f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the " + f"S3 encryption client" ) # The stored EC, minus default key/values, MUST match provided EC @@ -96,14 +112,16 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): if encryption_context_stored_copy != encryption_context_from_request: # TODO: modeled error raise S3EncryptionClientError( - "Provided encryption context does not match information retrieved from S3" + "Provided encryption context does not match information " + "retrieved from S3" ) # Update decMaterials with the modified encryption context elif edk.key_provider_info == "kms": if not self.enable_legacy_wrapping_algorithms: raise S3EncryptionClientError( - f"Enable legacy wrapping algorithms to use legacy key wrapping algorithm: {edk.key_provider_info}" + f"Enable legacy wrapping algorithms to use legacy key wrapping " + f"algorithm: {edk.key_provider_info}" ) else: raise S3EncryptionClientError( diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py index 1ab235da..9a2727f9 100644 --- a/src/s3_encryption/materials/materials.py +++ b/src/s3_encryption/materials/materials.py @@ -1,5 +1,11 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Materials module for S3 Encryption Client. + +This module provides classes for encryption and decryption materials, +which contain the cryptographic materials needed for S3 object encryption +and decryption operations. +""" from typing import Any from attrs import define, field diff --git a/src/s3_encryption/metadata.py b/src/s3_encryption/metadata.py index c59370bb..b4378990 100644 --- a/src/s3_encryption/metadata.py +++ b/src/s3_encryption/metadata.py @@ -1,5 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Metadata handling for S3 Encryption Client. + +This module provides classes and utilities for managing encryption metadata +for S3 objects, including serialization and deserialization of metadata. +""" import json from typing import Any @@ -16,7 +21,8 @@ class ObjectMetadata: All fields are optional and correspond to the following S3 encryption headers: - encrypted_data_key_v1: The encrypted data key (legacy format) - encrypted_data_key_v2: The encrypted data key (current format) - - encrypted_data_key_algorithm: The algorithm used to encrypt the data key (e.g. AES/GCM or kms+context) + - encrypted_data_key_algorithm: The algorithm used to encrypt the data key + (e.g. AES/GCM or kms+context) - encrypted_data_key_context: The encryption context used for the data key - content_iv: The initialization vector used for content encryption - content_cipher: The cipher algorithm used for content encryption (e.g. AES/GCM/NoPadding) diff --git a/src/s3_encryption/pipelines.py b/src/s3_encryption/pipelines.py index 6ce08661..6046fb3a 100644 --- a/src/s3_encryption/pipelines.py +++ b/src/s3_encryption/pipelines.py @@ -1,5 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +"""Encryption and decryption pipelines for S3 Encryption Client. + +This module provides pipelines for encrypting objects before they are put into S3 +and decrypting objects after they are retrieved from S3. +""" import base64 import os @@ -26,7 +31,7 @@ def encrypt(self, plaintext, encryption_context=None): """Encrypt the data before it is stored in S3. Args: - data (bytes or str): The data to be encrypted + plaintext (bytes or str): The data to be encrypted encryption_context (dict, optional): Additional context for encryption Returns: @@ -85,7 +90,7 @@ class GetEncryptedObjectPipeline: cmm: AbstractCryptoMaterialsManager = field() - def decrypt(self, response, encryption_context={}): + def decrypt(self, response, encryption_context=None): """Decrypt the data after it is retrieved from S3. Args: @@ -100,6 +105,10 @@ def decrypt(self, response, encryption_context={}): encryption_metadata = response.get("Metadata", {}) metadata = ObjectMetadata.from_dict(encryption_metadata) + # Use empty dict if encryption_context is None + if encryption_context is None: + encryption_context = {} + iv_b64 = metadata.content_iv edk_b64 = metadata.encrypted_data_key_v2 @@ -141,6 +150,4 @@ def decrypt(self, response, encryption_context={}): aesgcm = AESGCM(dec_materials.plaintext_data_key) - plaintext = aesgcm.decrypt(nonce=iv_bytes, data=encrypted_data, associated_data=None) - - return plaintext + return aesgcm.decrypt(nonce=iv_bytes, data=encrypted_data, associated_data=None) From 6c0b92a6d4679ee6fe90c9e73d3c95e7aefa7ebc Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 09:53:30 -0700 Subject: [PATCH 31/36] enforce ruff for src --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 47ace280..814ab334 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,9 @@ install: # Run linting checks lint: uv run black --check . - # Allow ruff to fail for now as we're gradually adopting linting standards - uv run ruff check src/ test/ || true + # Enforce ruff checks on src/ but allow test/ to fail + uv run ruff check src/ + uv run ruff check test/ || true # Format code with Black and Ruff format: From bee921849d630f3bb8a3de92dd386034ecb583fc Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 09:54:38 -0700 Subject: [PATCH 32/36] now fix black --- src/s3_encryption/__init__.py | 1 + src/s3_encryption/exceptions.py | 2 ++ src/s3_encryption/materials/crypto_materials_manager.py | 2 ++ src/s3_encryption/materials/kms_keyring.py | 1 + test/test_decryption_materials.py | 1 - test/test_decryption_materials_integration.py | 1 - test/test_encryption_materials.py | 1 - test/test_encryption_materials_integration.py | 1 - test/test_metadata.py | 1 - 9 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index 31ccf0a6..bf111d7b 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -33,6 +33,7 @@ class S3EncryptionClient: This client wraps a boto3 S3 client and provides encryption and decryption capabilities for S3 objects using the configured keyring and crypto materials manager. """ + wrapped_s3_client = field() config: S3EncryptionClientConfig = field() diff --git a/src/s3_encryption/exceptions.py b/src/s3_encryption/exceptions.py index d7d44df2..748075fc 100644 --- a/src/s3_encryption/exceptions.py +++ b/src/s3_encryption/exceptions.py @@ -4,5 +4,7 @@ This module contains custom exception classes used throughout the S3 Encryption Client. """ + + class S3EncryptionClientError(Exception): """Exception class for S3 Encryption Client errors.""" diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index 3ecb2489..349237f3 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -21,6 +21,7 @@ class AbstractCryptoMaterialsManager(abc.ABC): A crypto materials manager is responsible for generating encryption materials and processing decryption materials using a keyring. """ + @abc.abstractmethod def get_encryption_materials(self, enc_mats_request): """Get encryption materials from the keyring. @@ -57,6 +58,7 @@ class DefaultCryptoMaterialsManager(AbstractCryptoMaterialsManager): Attributes: keyring (AbstractKeyring): The keyring to use for cryptographic operations """ + keyring: AbstractKeyring def get_encryption_materials(self, enc_mats_request): diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 303a5f93..c663d264 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -27,6 +27,7 @@ class KmsKeyring(S3Keyring): kms_key_id (str): The KMS key ID to use enable_legacy_wrapping_algorithms (bool): Whether to enable legacy wrapping algorithms """ + kms_client = field() kms_key_id: str = field() enable_legacy_wrapping_algorithms: bool = field(default=False) diff --git a/test/test_decryption_materials.py b/test/test_decryption_materials.py index 94cd0c4e..aa544cee 100644 --- a/test/test_decryption_materials.py +++ b/test/test_decryption_materials.py @@ -1,7 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey from src.s3_encryption.materials.materials import DecryptionMaterials diff --git a/test/test_decryption_materials_integration.py b/test/test_decryption_materials_integration.py index 7a20e163..1cfab083 100644 --- a/test/test_decryption_materials_integration.py +++ b/test/test_decryption_materials_integration.py @@ -1,7 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest from unittest.mock import MagicMock, patch from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager diff --git a/test/test_encryption_materials.py b/test/test_encryption_materials.py index bd180d59..9aff14b0 100644 --- a/test/test_encryption_materials.py +++ b/test/test_encryption_materials.py @@ -1,7 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest from src.s3_encryption.materials.encrypted_data_key import EncryptedDataKey from src.s3_encryption.materials.materials import EncryptionMaterials diff --git a/test/test_encryption_materials_integration.py b/test/test_encryption_materials_integration.py index abac6f8c..989d17d8 100644 --- a/test/test_encryption_materials_integration.py +++ b/test/test_encryption_materials_integration.py @@ -1,7 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest from unittest.mock import MagicMock from src.s3_encryption.materials.crypto_materials_manager import DefaultCryptoMaterialsManager diff --git a/test/test_metadata.py b/test/test_metadata.py index 2abd2866..a061c185 100644 --- a/test/test_metadata.py +++ b/test/test_metadata.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import os import sys -import pytest # Add the src directory to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) From 4b5ab01a6086666e4d21ba0eabfcb6bef3bc45d2 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 10:45:34 -0700 Subject: [PATCH 33/36] empty body plus tests --- src/s3_encryption/__init__.py | 5 +- test/integration/test_i_s3_encryption.py | 61 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/s3_encryption/__init__.py b/src/s3_encryption/__init__.py index bf111d7b..62ca99e6 100644 --- a/src/s3_encryption/__init__.py +++ b/src/s3_encryption/__init__.py @@ -45,7 +45,8 @@ def put_object(self, **kwargs): Args: **kwargs: Arguments to pass to the S3 client's put_object method. - Must include Bucket, Key, and Body parameters. + Must include Bucket and Key parameters. + Body parameter is optional; if not provided, an empty object is uploaded. May include EncryptionContext for additional authenticated data. Returns: @@ -54,7 +55,7 @@ def put_object(self, **kwargs): # Extract required parameters from kwargs bucket = kwargs.pop("Bucket") key = kwargs.pop("Key") - body = kwargs.pop("Body") + body = kwargs.pop("Body", b"") # Default to empty bytes when Body is not provided encryption_context = kwargs.pop("EncryptionContext", None) # Create a pipeline for this operation diff --git a/test/integration/test_i_s3_encryption.py b/test/integration/test_i_s3_encryption.py index 14692c41..99f25f85 100644 --- a/test/integration/test_i_s3_encryption.py +++ b/test/integration/test_i_s3_encryption.py @@ -40,3 +40,64 @@ def test_simple_roundtrip(): print(output) raise RuntimeError print("Success!") + + +def test_empty_string_roundtrip(): + key = "empty-string-rt" + key += datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + + data = "" # Empty string as test data + + kms_client = boto3.client("kms", region_name=region) + + keyring = KmsKeyring(kms_client, kms_key_id) + + wrapped_client = boto3.client("s3") + config = S3EncryptionClientConfig(keyring) + s3ec = S3EncryptionClient(wrapped_client, config) + s3ec.put_object(Bucket=bucket, Key=key, Body=data) + get_req = {"Bucket": bucket, "Key": key} + response = s3ec.get_object(**get_req) + output = response["Body"].read().decode("utf-8") + if output != data: + print("Uh oh! Input and output don't match!") + print("Input:") + print(repr(data)) # Using repr to clearly show it's an empty string + print("Output:") + print(repr(output)) + raise RuntimeError + print("Success! Empty string encrypted and decrypted correctly.") + + +def test_no_body_roundtrip(): + key = "no-body-rt" + key += datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + + # Expected data when no Body is provided (empty bytes) + expected_data = b"" + + kms_client = boto3.client("kms", region_name=region) + + keyring = KmsKeyring(kms_client, kms_key_id) + + wrapped_client = boto3.client("s3") + config = S3EncryptionClientConfig(keyring) + s3ec = S3EncryptionClient(wrapped_client, config) + + # Call put_object without providing a Body parameter + s3ec.put_object(Bucket=bucket, Key=key) + + get_req = {"Bucket": bucket, "Key": key} + response = s3ec.get_object(**get_req) + output = response["Body"].read() + + if output != expected_data: + print("Uh oh! Output doesn't match expected empty bytes!") + print("Expected:") + print(repr(expected_data)) + print("Output:") + print(repr(output)) + raise RuntimeError + print( + "Success! Object with no Body parameter encrypted and decrypted correctly as empty bytes." + ) From c440af4b35255dcf24f63d3795ed7fb9b95c4994 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 10:51:30 -0700 Subject: [PATCH 34/36] fix type hints --- src/s3_encryption/materials/crypto_materials_manager.py | 3 ++- src/s3_encryption/materials/keyring.py | 2 +- src/s3_encryption/materials/kms_keyring.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/s3_encryption/materials/crypto_materials_manager.py b/src/s3_encryption/materials/crypto_materials_manager.py index 349237f3..82eab454 100644 --- a/src/s3_encryption/materials/crypto_materials_manager.py +++ b/src/s3_encryption/materials/crypto_materials_manager.py @@ -65,7 +65,8 @@ def get_encryption_materials(self, enc_mats_request): """Get encryption materials from the keyring. Args: - enc_mats_request (Dict[str, Any]): Request containing encryption parameters + enc_mats_request (Dict[str, Any] or EncryptionMaterials): Request containing encryption + parameters Returns: EncryptionMaterials: The encryption materials diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 6115b02f..7c5b3bf8 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -27,7 +27,7 @@ def on_encrypt(self, enc_materials): """Process encryption materials. Args: - enc_materials (EncryptionMaterials): Encryption materials to process + enc_materials (EncryptionMaterials or dict): Encryption materials to process Returns: EncryptionMaterials: The processed encryption materials diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index c663d264..206b0e92 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -36,7 +36,7 @@ def on_encrypt(self, enc_materials): """Process encryption materials using KMS. Args: - enc_materials (EncryptionMaterials): Encryption materials to process + enc_materials (EncryptionMaterials or dict): Encryption materials to process Returns: EncryptionMaterials: The processed encryption materials with KMS-generated keys From d113d54cc79ff9bc9a1ff365bf14e517f76c4a8d Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 10:57:45 -0700 Subject: [PATCH 35/36] try client type hint again --- src/s3_encryption/materials/kms_keyring.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 206b0e92..0715cc8b 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -7,6 +7,7 @@ """ from attrs import define, field +from botocore import client from ..exceptions import S3EncryptionClientError from .encrypted_data_key import EncryptedDataKey @@ -23,12 +24,12 @@ class KmsKeyring(S3Keyring): This keyring uses AWS KMS to generate and decrypt data keys. Attributes: - kms_client: The boto3 KMS client + kms_client (client.BaseClient): The boto3 KMS client kms_key_id (str): The KMS key ID to use enable_legacy_wrapping_algorithms (bool): Whether to enable legacy wrapping algorithms """ - kms_client = field() + kms_client: client.BaseClient = field() kms_key_id: str = field() enable_legacy_wrapping_algorithms: bool = field(default=False) From a73d544ed7b5a68249cf2f5b352ca634e422e816 Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Tue, 12 Aug 2025 11:10:17 -0700 Subject: [PATCH 36/36] PDK to plaintext_data_key --- src/s3_encryption/materials/keyring.py | 2 +- src/s3_encryption/materials/kms_keyring.py | 2 +- src/s3_encryption/materials/materials.py | 12 ++++++------ test/test_decryption_materials.py | 4 ++-- test/test_encryption_materials.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/s3_encryption/materials/keyring.py b/src/s3_encryption/materials/keyring.py index 7c5b3bf8..1e08cb18 100644 --- a/src/s3_encryption/materials/keyring.py +++ b/src/s3_encryption/materials/keyring.py @@ -45,7 +45,7 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): keys to try. Returns: - DecryptionMaterials: The updated dec_materials with the plaintext data key (PDK) + DecryptionMaterials: The updated dec_materials with the plaintext data key """ pass diff --git a/src/s3_encryption/materials/kms_keyring.py b/src/s3_encryption/materials/kms_keyring.py index 0715cc8b..7bc8f7bd 100644 --- a/src/s3_encryption/materials/kms_keyring.py +++ b/src/s3_encryption/materials/kms_keyring.py @@ -75,7 +75,7 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None): keys to try. Returns: - DecryptionMaterials: The updated dec_materials with the plaintext data key (PDK) + DecryptionMaterials: The updated dec_materials with the plaintext data key """ try: # Call parent class validation diff --git a/src/s3_encryption/materials/materials.py b/src/s3_encryption/materials/materials.py index 9a2727f9..9f72ab91 100644 --- a/src/s3_encryption/materials/materials.py +++ b/src/s3_encryption/materials/materials.py @@ -23,7 +23,7 @@ class EncryptionMaterials: Attributes: encryption_context (Dict[str, str]): Context information for encryption encrypted_data_key (Optional[EncryptedDataKey]): The encrypted data key - plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) + plaintext_data_key (Optional[bytes]): The plaintext data key """ encryption_context: dict[str, str] = field(factory=dict) @@ -43,7 +43,7 @@ def from_dict(cls, materials_dict: dict[str, Any]) -> "EncryptionMaterials": return cls( encryption_context=materials_dict.get("encryption_context", {}), encrypted_data_key=materials_dict.get("encrypted_data_key"), - plaintext_data_key=materials_dict.get("PDK"), + plaintext_data_key=materials_dict.get("plaintext_data_key"), ) def to_dict(self) -> dict[str, Any]: @@ -61,7 +61,7 @@ def to_dict(self) -> dict[str, Any]: result["encrypted_data_key"] = self.encrypted_data_key if self.plaintext_data_key is not None: - result["PDK"] = self.plaintext_data_key + result["plaintext_data_key"] = self.plaintext_data_key return result @@ -78,7 +78,7 @@ class DecryptionMaterials: encrypted_data_keys (List[EncryptedDataKey]): List of encrypted data keys to try encryption_context_stored (Dict[str, str]): Encryption context stored with the object encryption_context_from_request (Dict[str, str]): Encryption context provided in the request - plaintext_data_key (Optional[bytes]): The plaintext data key (PDK) + plaintext_data_key (Optional[bytes]): The plaintext data key """ iv: bytes | None = field(default=None) @@ -104,7 +104,7 @@ def from_dict(cls, materials_dict: dict[str, Any]) -> "DecryptionMaterials": encryption_context_from_request=materials_dict.get( "encryption_context_from_request", {} ), - plaintext_data_key=materials_dict.get("PDK"), + plaintext_data_key=materials_dict.get("plaintext_data_key"), ) def to_dict(self) -> dict[str, Any]: @@ -128,6 +128,6 @@ def to_dict(self) -> dict[str, Any]: result["encryption_context_from_request"] = self.encryption_context_from_request if self.plaintext_data_key is not None: - result["PDK"] = self.plaintext_data_key + result["plaintext_data_key"] = self.plaintext_data_key return result diff --git a/test/test_decryption_materials.py b/test/test_decryption_materials.py index aa544cee..c160b509 100644 --- a/test/test_decryption_materials.py +++ b/test/test_decryption_materials.py @@ -56,7 +56,7 @@ def test_from_dict(self): "encrypted_data_keys": [edk], "encryption_context_stored": {"key1": "value1"}, "encryption_context_from_request": {"key2": "value2"}, - "PDK": b"plaintext-data-key", + "plaintext_data_key": b"plaintext-data-key", } materials = DecryptionMaterials.from_dict(materials_dict) assert materials.iv == b"initialization-vector" @@ -84,4 +84,4 @@ def test_to_dict(self): assert materials_dict["encrypted_data_keys"] == [edk] assert materials_dict["encryption_context_stored"] == {"key1": "value1"} assert materials_dict["encryption_context_from_request"] == {"key2": "value2"} - assert materials_dict["PDK"] == b"plaintext-data-key" + assert materials_dict["plaintext_data_key"] == b"plaintext-data-key" diff --git a/test/test_encryption_materials.py b/test/test_encryption_materials.py index 9aff14b0..54d80146 100644 --- a/test/test_encryption_materials.py +++ b/test/test_encryption_materials.py @@ -30,7 +30,7 @@ def test_from_dict(self): materials_dict = { "encryption_context": {"key1": "value1"}, "encrypted_data_key": edk, - "PDK": b"plaintext-data-key", + "plaintext_data_key": b"plaintext-data-key", } materials = EncryptionMaterials.from_dict(materials_dict) assert materials.encryption_context == {"key1": "value1"} @@ -52,4 +52,4 @@ def test_to_dict(self): materials_dict = materials.to_dict() assert materials_dict["encryption_context"] == {"key1": "value1"} assert materials_dict["encrypted_data_key"] == edk - assert materials_dict["PDK"] == b"plaintext-data-key" + assert materials_dict["plaintext_data_key"] == b"plaintext-data-key"