├── main.py ├── app ├── __init__.py ├── ai_generator │ ├── __init__.py │ ├── requirements.txt │ └── index.py └── tests │ ├── conftest.py │ └── test_ai_generator.py ├── cdk ├── tests │ ├── __init__.py │ └── test_ai_security_recommendations_stack.py ├── l3constructs │ ├── lambda_functions │ │ ├── __init__.py │ │ ├── README.md │ │ ├── L3Lambda.py │ │ ├── L3LambdaJava.py │ │ └── L3LambdaPython.py │ ├── helpers │ │ ├── base_stack.py │ │ └── helper.py │ └── s3 │ │ └── l3_bucket.py ├── constants.py ├── app.py ├── cdk.json └── stacks │ └── ai_security_recommendations.py ├── docs └── Architecture.png ├── .flake8 ├── tox.ini ├── .gitignore ├── scripts └── cleanup_cdk_bootstrap.py ├── pyproject.toml ├── README.md ├── .pre-commit-config.yaml ├── Makefile └── poetry.lock /main.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdk/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/ai_generator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdk/l3constructs/lambda_functions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/ai_generator/requirements.txt: -------------------------------------------------------------------------------- 1 | fpdf==1.7.2 2 | -------------------------------------------------------------------------------- /docs/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/ai_powered_threat_intelligence/main/docs/Architecture.png -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Recommend matching the black line length (default 88), 3 | # rather than using the flake8 default of 79: 4 | max-line-length = 150 5 | exclude = cdk/cdk.out 6 | jobs = 8 7 | ignore = E203, BLK100, W503 8 | -------------------------------------------------------------------------------- /cdk/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | File for storing constants used throughout the code 3 | """ 4 | TAGS = [] 5 | BEDROCK_MODEL_ID = "anthropic.claude-3-5-sonnet-20240620-v1:0" 6 | LAMBDA_PILLOW_LAYER = "arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p312-Pillow:3" 7 | ANTROPIC_VERSION = "bedrock-2023-05-31" 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox.ini 2 | [tox] 3 | envlist = py312 # Specify Python versions you want to support (adjust as necessary) 4 | skipsdist = True # Skips packaging, faster for local testing 5 | isolated_build = True # Isolate the environment 6 | 7 | [testenv] 8 | deps = 9 | pytest 10 | pytest-cov 11 | moto # Add any dependencies needed for tests (e.g., moto for mocking boto3) 12 | boto3 # Example dependency for your app 13 | fpdf 14 | attr 15 | aws-cdk-lib 16 | cdk_nag 17 | pygit2 18 | assertpy 19 | 20 | commands = 21 | python -m pytest -v . 22 | -------------------------------------------------------------------------------- /cdk/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | CDK app that creates resources. 5 | """ 6 | 7 | import aws_cdk as cdk 8 | import cdk_nag 9 | import constants 10 | from l3constructs.helpers.helper import Helper 11 | from stacks.ai_security_recommendations import AISecurityRecommendations 12 | 13 | helper = Helper(tags=constants.TAGS) 14 | 15 | app = cdk.App(context={"@aws-cdk/core:bootstrapQualifier": helper.get_qualifier()}) 16 | AISecurityRecommendations(app, "AiSecurityRecommendations") 17 | 18 | # Run cdk nag checks on CDK app scope 19 | cdk.Aspects.of(app).add(cdk_nag.AwsSolutionsChecks(verbose=True)) 20 | 21 | app.synth() 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .hypothesis/ 3 | .idea/* 4 | .pytest_cache/ 5 | .scannerwork/ 6 | .vscode/ 7 | cdk.out/ 8 | coverage/ 9 | htmlcov/ 10 | lambda_python/layers/*/*/ 11 | __pycache__/ 12 | .venv 13 | env 14 | 15 | # Files 16 | *.d.ts 17 | *.pyc 18 | *.zip 19 | .DS_Store 20 | .idea 21 | .coverage 22 | .npmrc 23 | .nvmrc 24 | bandit.json 25 | cdk/cdk.context.json 26 | coverage.xml 27 | package-lock.json 28 | safety.json 29 | semgrep.json 30 | test-report.xml 31 | awstoe 32 | ignoreme.bat 33 | cdk/floating_license_server/resources/api/Dockerfile* 34 | 35 | # License reports 36 | license_report 37 | license_report*.xlsx 38 | 39 | # CDK asset staging directory 40 | .cdk.staging 41 | cdk.out 42 | cdk/cdk.context.json 43 | .idea/aws.xml 44 | .tox 45 | -------------------------------------------------------------------------------- /cdk/l3constructs/helpers/base_stack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base stack contains a class that modifies the regular Stack functionality 3 | by adding custom logic to it. 4 | """ 5 | 6 | import constants 7 | from aws_cdk import Stack 8 | from constructs import Construct 9 | from l3constructs.helpers.helper import Helper 10 | 11 | 12 | class BaseStack(Stack): 13 | """ 14 | BaseStack class inherits from the default CDK Stack construct and 15 | adds custom logic to it, to meet project requirements. 16 | """ 17 | 18 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 19 | helper = Helper(tags=constants.TAGS) 20 | 21 | cdk_env = helper.get_cdk_env() 22 | qualifier = helper.get_qualifier() 23 | 24 | # construct_id = f"{qualifier}-{construct_id}" 25 | construct_id = f"{construct_id}" 26 | 27 | # Set class attributes available in implementing stacks via self. 28 | self.qualifier = qualifier 29 | self.cdk_env = cdk_env 30 | self.prefix = helper.prefix 31 | 32 | super().__init__(scope, construct_id, **kwargs) 33 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 25 | "@aws-cdk/core:checkSecretUsage": true, 26 | "@aws-cdk/aws-iam:minimizePolicies": true, 27 | "@aws-cdk/aws-ec2:noSubnetRouteTableId": true, 28 | "@aws-cdk/aws-autoscaling:desiredCapacitySet": true, 29 | "@aws-cdk/core:target-partitions": [ 30 | "aws" 31 | ], 32 | "app-name": "AIEnhancedSecurity" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/cleanup_cdk_bootstrap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | import boto3 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | qualifier = sys.argv[1] 10 | stack_name = sys.argv[2] 11 | # Set default values 12 | account = None 13 | region = None 14 | # Check if region argument is provided 15 | if len(sys.argv) > 4: 16 | account = sys.argv[3] if sys.argv[3] != "" else None 17 | region = sys.argv[4] if sys.argv[4] != "" else None 18 | 19 | s3 = boto3.resource("s3", region_name=region) 20 | cloudformation = boto3.resource("cloudformation", region_name=region) 21 | 22 | account = account if account else boto3.client("sts").get_caller_identity()["Account"] 23 | region = region if region else boto3.session.Session().region_name 24 | 25 | try: 26 | logger.info(f"Deleting stack {stack_name} in account {account} and region {region}") 27 | stack = cloudformation.Stack(stack_name) 28 | stack.delete() 29 | logger.info(f"Deleting S3 bucket cdk-{qualifier}-assets-{account}-{region}") 30 | bucket = s3.Bucket(f"cdk-{qualifier}-assets-{account}-{region}") 31 | bucket.objects.delete() 32 | bucket.object_versions.delete() 33 | bucket.delete() 34 | logger.info(f"Stack {stack_name} deleted successfully") 35 | except s3.meta.client.exceptions.NoSuchBucket: 36 | pass 37 | -------------------------------------------------------------------------------- /cdk/l3constructs/lambda_functions/README.md: -------------------------------------------------------------------------------- 1 | # L3LambdaPython CDK Construct 2 | 3 | ## Overview 4 | 5 | The L3LambdaPython construct is a high-level (L3) AWS CDK construct that simplifies the process of creating and configuring AWS Lambda functions using Python. This construct provides a convenient way to set up Lambda functions with common configurations and best practices. 6 | 7 | ## Features 8 | 9 | - Easy creation of Python-based Lambda functions 10 | - Automatic handling of dependencies and packaging 11 | - Configurable memory and timeout settings 12 | - Built-in logging configuration 13 | - Optional VPC and security group configuration 14 | - Simplified IAM role and policy management 15 | 16 | ## Installation 17 | 18 | To use this construct in your CDK project, ensure you have the AWS CDK installed and then install this construct: 19 | 20 | ## Usage 21 | 22 | ``` 23 | from aws_cdk import Stack 24 | from constructs import Construct 25 | from your_module import L3LambdaPython 26 | 27 | class MyStack(Stack): 28 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 29 | super().__init__(scope, construct_id, **kwargs) 30 | 31 | lambda_function = L3LambdaPython( 32 | self, 33 | "MyLambdaFunction", 34 | function_name="my-lambda-function", 35 | handler="index.handler", 36 | code_dir="./lambda", 37 | memory_size=256, 38 | timeout=30 39 | ) 40 | 41 | # You can now use lambda_function for further configurations or integrations 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /cdk/l3constructs/s3/l3_bucket.py: -------------------------------------------------------------------------------- 1 | # s3_bucket.py 2 | 3 | from aws_cdk import RemovalPolicy 4 | from aws_cdk import aws_s3 as s3 5 | from constructs import Construct 6 | 7 | 8 | class L3S3Bucket(Construct): 9 | def __init__( 10 | self, scope: Construct, id: str, *, bucket_name: str, log_bucket_name: str, **kwargs 11 | ) -> None: 12 | super().__init__(scope, id, **kwargs) 13 | 14 | # Create a log bucket for server access logging 15 | self.log_bucket = s3.Bucket( 16 | self, 17 | log_bucket_name, 18 | versioned=True, 19 | removal_policy=RemovalPolicy.DESTROY, 20 | encryption=s3.BucketEncryption.S3_MANAGED, 21 | enforce_ssl=True, 22 | public_read_access=False, 23 | ) 24 | 25 | # Create the main S3 bucket 26 | self.bucket = s3.Bucket( 27 | self, 28 | bucket_name, 29 | versioned=True, 30 | public_read_access=False, 31 | block_public_access=s3.BlockPublicAccess.BLOCK_ALL, 32 | lifecycle_rules=kwargs.get("lifecycle_rules", []), 33 | server_access_logs_bucket=self.log_bucket, 34 | server_access_logs_prefix="access-logs/", 35 | encryption=s3.BucketEncryption.S3_MANAGED, 36 | enforce_ssl=True, 37 | ) 38 | 39 | def get_bucket(self) -> s3.IBucket: 40 | """Return the bucket instance""" 41 | return self.bucket 42 | 43 | def get_log_bucket(self) -> s3.IBucket: 44 | """Return the log bucket instance""" 45 | return self.log_bucket 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aod-project" 3 | version = "1.0.0" 4 | description = "CDK project for creating resources and components" 5 | authors = [ 6 | "Jose Nunes ", 7 | ] 8 | readme = "README.md" 9 | package-mode=false 10 | 11 | [tool.bandit] 12 | exclude_dirs = [".venv", "cdk/cdk.out" ] 13 | verbose = true 14 | skips = ["B101"] 15 | 16 | [tool.black] 17 | line-length = 100 18 | preview = true 19 | skip-magic-trailing-comma = true 20 | 21 | 22 | [tool.isort] 23 | profile = "black" 24 | line_length = 100 25 | 26 | [tool.pylint] 27 | # UPDATE: Aligned with flake8 and black 28 | max-line-length = 100 29 | fail-under = 8 30 | ignore-paths = "cdk/cdk.out/*" 31 | 32 | [tool.pylint.messages_control] 33 | disable = [ 34 | "E0401", 35 | "R0801", 36 | "R0903", 37 | "R0913", 38 | "R0914", 39 | "W0602", 40 | "W0603" 41 | ] 42 | 43 | [tool.poetry.dependencies] 44 | python = ">=3.10,<4.0" 45 | aws-cdk-lib = "2.130.0" 46 | boto3 = "1.34.51" 47 | cdk-nag = "2.28.48" 48 | constructs = "10.3.0" 49 | pre-commit = "3.6.2" 50 | fpdf = "^1.7.2" 51 | tox = "^4.23.2" 52 | pygit2 = "^1.16.0" 53 | 54 | [tool.poetry.group.security.dependencies] 55 | pip-audit = "2.7.1" 56 | 57 | [tool.poetry.group.lint.dependencies] 58 | flake8 = "7.1.1" 59 | black = "24.10.0" 60 | pylint = "3.3.0" 61 | 62 | [tool.poetry.group.test.dependencies] 63 | coverage = "7.4.3" 64 | pytest = "8.0.2" 65 | pytest-cov = "4.1.0" 66 | assertpy = "1.1" 67 | pytest-env = "^1.1.3" 68 | 69 | [tool.yamlfix] 70 | line_length = 240 71 | sequence_style = "keep_style" 72 | 73 | [build-system] 74 | requires = ["poetry-core"] 75 | build-backend = "poetry.core.masonry.api" 76 | -------------------------------------------------------------------------------- /cdk/l3constructs/lambda_functions/L3Lambda.py: -------------------------------------------------------------------------------- 1 | from typing import List, Mapping, Optional 2 | 3 | from aws_cdk import Duration 4 | from aws_cdk import aws_ec2 as ec2 5 | from aws_cdk import aws_iam as iam 6 | from aws_cdk import aws_kms as kms 7 | from aws_cdk import aws_lambda as lambda_ 8 | from constructs import Construct 9 | 10 | 11 | class L3Lambda(lambda_.Function): 12 | def __init__( 13 | self, 14 | scope: Construct, 15 | id: str, 16 | handler: str, 17 | code_path: Optional[str] = None, 18 | code: Optional[lambda_.Code] = None, 19 | runtime: Optional[lambda_.Runtime] = None, 20 | role: Optional[iam.Role] = None, 21 | vpc: Optional[str] = None, 22 | memory_size: int = 128, 23 | timeout: Duration = Duration.seconds(60), 24 | description: Optional[str] = None, 25 | layers: Optional[List[lambda_.LayerVersion]] = None, 26 | environment: Optional[Mapping] = None, 27 | environment_encryption: Optional[kms.Key] = None, 28 | security_groups: Optional[List[ec2.SecurityGroup]] = None, 29 | ): 30 | super(L3Lambda, self).__init__( 31 | scope=scope, 32 | id=id, 33 | runtime=runtime, 34 | code=code or lambda_.Code.from_asset(code_path), 35 | handler=handler, 36 | role=role, 37 | vpc=vpc, 38 | memory_size=memory_size or 128, 39 | timeout=timeout, 40 | description=description, 41 | layers=layers, 42 | environment=environment, 43 | environment_encryption=environment_encryption, 44 | security_groups=security_groups, 45 | ) 46 | 47 | if environment_encryption: 48 | environment_encryption.grant_encrypt_decrypt(self) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Integrating AWS GuardDuty, Detective and Bedrock AI for Enhanced Threat Detection, Investigation and AI remediation recommendation. 2 | ## Brief Description 3 | As organizations grow and expand their cloud infrastructure across multiple AWS accounts, managing security operations becomes increasingly complex. Security teams face challenges in monitoring and analyzing a vast amount of security data from disparate accounts, identifying threats accurately, and responding to incidents promptly. Traditional security tools may lack the advanced intelligence and centralized visibility needed to combat sophisticated attacks across such expansive environments. Additionally, the manual effort required for compliance and auditing can burden security teams, limiting their capacity for proactive threat response. 4 | 5 | Without a unified, AI-powered approach, organizations are at risk of delayed threat detection, inefficient incident response, increased operational costs, and difficulty in maintaining a consistent security posture. This can lead to vulnerabilities in compliance, governance, and overall security resilience, especially in multi-account, large-scale AWS environments. 6 | ## Architecture 7 | 8 | ![Architecture.png](docs/Architecture.png) 9 | 10 | ## Code 11 | 12 | ### Structure 13 | - app 14 | - code of the actual application (Lambda function) including unit tests 15 | - cdk 16 | - code for the infrastructure including unit tests 17 | - docs 18 | - Documentation 19 | 20 | 21 | ### Pre-requisites 22 | 23 | - This is a python project as such python3.12 or higher is requirements 24 | - Poetry is needed to managed the python packages 25 | - To install poetry run 26 | - ``` 27 | pip install poetry 28 | ``` 29 | 30 | ### Setting up the environment 31 | - 1st validate that the pre-requisites are meet 32 | - Create a virtual Environment 33 | - ``` 34 | python3 -m venv .venv 35 | ``` 36 | - run the make command to install the python dependencies 37 | - ``` 38 | make install 39 | ``` 40 | 41 | 42 | 43 | ### Bootstrap tooling account 44 | ``` 45 | Ensure you have the right credentials exported into the environment 46 | run: make bootstrap 47 | ``` 48 | 49 | ### Deploy the AWS infrastructure to the destination account 50 | - This will deploy the infrastructure into the account. 51 | ``` 52 | make deploy 53 | ``` 54 | 55 | ### Destroy the AWS infrastructure to the destination account 56 | - This will destroy the infrastructure into the account. 57 | ``` 58 | make destroy 59 | ``` 60 | 61 | ### Destroy Bootstrap 62 | - Ensure you have the right credentials exported into the environment 63 | - Ensure that no stacks are remaining, as this will remove the cdk role and then the dependent stacks can't be deleted 64 | ``` 65 | run: make clean-bootstrap 66 | ``` 67 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: trailing-whitespace 7 | alias: lint 8 | - id: end-of-file-fixer 9 | alias: lint 10 | exclude: ^.*\.egg-info/ 11 | - id: check-merge-conflict 12 | alias: lint 13 | - id: check-case-conflict 14 | alias: lint 15 | - id: check-json 16 | alias: lint 17 | - id: check-toml 18 | alias: lint 19 | exclude: tests/fixtures/invalid_lock/poetry\.lock 20 | - id: check-yaml 21 | alias: lint 22 | args: [--unsafe] 23 | - id: pretty-format-json 24 | alias: lint 25 | args: [--autofix, --no-ensure-ascii, --no-sort-keys] 26 | - id: check-ast 27 | alias: lint 28 | - id: check-executables-have-shebangs 29 | alias: lint 30 | - id: check-added-large-files 31 | alias: lint 32 | args: [--maxkb=15000] 33 | - id: check-symlinks 34 | alias: lint 35 | - id: detect-aws-credentials 36 | alias: lint 37 | args: [--allow-missing-credentials] 38 | - id: debug-statements 39 | alias: lint 40 | - id: check-docstring-first 41 | alias: lint 42 | - id: check-builtin-literals 43 | alias: lint 44 | - id: end-of-file-fixer 45 | alias: lint 46 | - id: no-commit-to-branch 47 | name: no-commit-to-main 48 | args: [--branch, main] 49 | - id: mixed-line-ending 50 | alias: lint 51 | args: [--fix=lf] 52 | - id: requirements-txt-fixer 53 | - repo: https://github.com/lyz-code/yamlfix/ 54 | rev: 1.16.1 55 | hooks: 56 | - id: yamlfix 57 | alias: lint 58 | - repo: https://github.com/pycqa/isort 59 | rev: 5.12.0 60 | hooks: 61 | - id: isort 62 | alias: lint 63 | - repo: https://github.com/psf/black 64 | rev: 24.10.0 65 | hooks: 66 | - id: black 67 | alias: lint 68 | args: [--config, pyproject.toml] 69 | - repo: https://github.com/pycqa/flake8 70 | rev: 7.0.0 71 | hooks: 72 | - id: flake8 73 | alias: lint 74 | args: [--config, .flake8] 75 | additional_dependencies: 76 | - flake8-black>=0.3.6 77 | - repo: https://github.com/pre-commit/pre-commit 78 | rev: v2.21.0 79 | hooks: 80 | - id: validate_manifest 81 | alias: lint 82 | - repo: https://github.com/commitizen-tools/commitizen 83 | rev: v2.39.1 84 | hooks: 85 | - id: commitizen 86 | alias: lint 87 | - repo: https://github.com/python-poetry/poetry 88 | rev: 1.8.1 89 | hooks: 90 | - id: poetry-check 91 | alias: lint 92 | - id: poetry-lock 93 | alias: lock 94 | - repo: https://github.com/PyCQA/bandit 95 | rev: 1.7.5 96 | hooks: 97 | - id: bandit 98 | alias: security 99 | args: [-c, pyproject.toml, -ll, -r, cdk/] 100 | always_run: true 101 | additional_dependencies: ['bandit[toml]'] 102 | -------------------------------------------------------------------------------- /cdk/l3constructs/lambda_functions/L3LambdaJava.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | from typing import List, Mapping, Optional 5 | 6 | import aws_cdk 7 | from aws_cdk import Duration, Stack 8 | from aws_cdk import aws_iam as iam 9 | from aws_cdk import aws_kms, aws_lambda 10 | from constructs import Construct 11 | from jsii import implements, member 12 | 13 | from .L3Lambda import L3Lambda 14 | 15 | 16 | @implements(aws_cdk.ILocalBundling) 17 | class MyLocalBundler: 18 | def __init__(self, lambda_root: str) -> None: 19 | self._lambda_root = lambda_root 20 | 21 | @member(jsii_name="tryBundle") 22 | def try_bundle(self, output_dir: str, options: aws_cdk.BundlingOptions) -> bool: 23 | if os.environ.get("OS", "") == "Windows_NT": 24 | subprocess.run([f"cd {self._lambda_root}", "mvn clean install"]) 25 | else: 26 | subprocess.run( 27 | ["bash", "-c", " && ".join([f"cd {self._lambda_root}, mvn clean install"])] 28 | ) 29 | return True 30 | 31 | 32 | class L3LambdaJava(L3Lambda): 33 | def __init__( 34 | self, 35 | scope: Construct, 36 | id: str, 37 | code: str, 38 | handler: str, 39 | function_name: str, 40 | runtime: aws_lambda.Runtime = aws_lambda.Runtime.JAVA_21, 41 | role: Optional[iam.Role] = None, 42 | vpc: Optional[str] = None, 43 | environment: Optional[Mapping] = None, 44 | environment_encryption: Optional[aws_kms.Key] = None, 45 | security_groups: Optional[list] = None, 46 | layers: Optional[List[aws_lambda.LayerVersion]] = None, 47 | description: Optional[str] = None, 48 | memory: int = 128, 49 | timeout: Duration = Duration.seconds(60), 50 | ): 51 | super(L3LambdaJava, self).__init__( 52 | scope=scope, 53 | id=id, 54 | code=L3LambdaJava.bundle_locally( 55 | lambda_root=code, runtime=runtime, function_name=function_name 56 | ), 57 | handler=handler, 58 | runtime=runtime or None, 59 | role=role or None, 60 | vpc=vpc or None, 61 | description=description or None, 62 | layers=layers or None, 63 | environment=environment or None, 64 | environment_encryption=environment_encryption or None, 65 | security_groups=security_groups or None, 66 | timeout=timeout, 67 | memory_size=memory, 68 | ) 69 | 70 | @staticmethod 71 | def format_dockerfile( 72 | scope: Construct, 73 | id: str, 74 | code: str, 75 | runtime: aws_lambda.Runtime, 76 | docker_file: str = "Dockerfile.python", 77 | ) -> aws_lambda.Code: 78 | input_docker_file_path = f"./cdk/l3_constructs/lambda_functions/docker_files/{docker_file}" 79 | output_docker_file_name = f"Dockerfile.{Stack.of(scope).stack_name}.{id}" 80 | output_docker_file_path = f"{code}/{output_docker_file_name}" 81 | 82 | with open(input_docker_file_path, "r") as docker_file_r: 83 | file = docker_file_r.read() 84 | file_content = file.format( 85 | **{"RUNTIME": re.sub(r"([a-z_-]+)", r"\g<1>:", runtime.to_string(), 1)} 86 | ) 87 | 88 | with open(output_docker_file_path, "w") as docker_file_w: 89 | docker_file_w.write(file_content) 90 | 91 | docker_build = aws_lambda.Code.from_docker_build( 92 | path=os.path.abspath(code), file=output_docker_file_name, platform="linux/amd64" 93 | ) 94 | 95 | return docker_build 96 | 97 | @staticmethod 98 | def bundle_locally(lambda_root: str, runtime: aws_lambda.Runtime, function_name: str): 99 | asset_hash = aws_cdk.FileSystem.fingerprint(lambda_root) 100 | 101 | current_dir = "." 102 | code = aws_lambda.Code.from_asset( 103 | path=current_dir, 104 | bundling=aws_cdk.BundlingOptions( 105 | image=runtime.bundling_image, 106 | command=[ 107 | f"rsync -r {lambda_root}/target/{function_name}.jar" 108 | f" /asset-output/{lambda_root}/" 109 | ], 110 | local=MyLocalBundler(lambda_root=lambda_root), 111 | ), 112 | asset_hash=asset_hash, 113 | asset_hash_type=aws_cdk.AssetHashType.CUSTOM, 114 | ) 115 | return code 116 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help clean clean-build clean-pyc clean-test clean-venv lint coverage test synth ls diff install bootstrap-tooling bootstrap-target deploy-pipeline destroy-pipeline 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | ifeq ($(OS),Windows_NT) 27 | SHELL := powershell.exe 28 | .SHELLFLAGS := -NoProfile -Command 29 | endif 30 | 31 | help: 32 | ifeq ($(OS),Windows_NT) 33 | foreach($$line in $$(get-content Makefile | Select-String -Pattern "^([a-zA-Z_-]+):.*?## (.*)`$$`$$")) {write-host "$$("{0, -20}" -f $$($$line -split ":")[0]) $$($$($$line -split "##")[-1])"} 34 | else 35 | @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 36 | endif 37 | 38 | clean: clean-cdk clean-pyc clean-test ## Remove all build, test, coverage and Python artifacts 39 | 40 | clean-cdk: ## Remove cdk artifacts 41 | ifeq ($(OS),Windows_NT) 42 | Get-ChildItem * -Include cdk/cdk.out -Recurse | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue; 43 | Get-ChildItem * -Include cdk/cdk.context.json -Recurse | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue; 44 | Get-ChildItem * -Include cdk/package-lock.json -Recurse | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue; 45 | else 46 | rm -fr cdk/cdk.out 47 | rm -f cdk/cdk.context.json 48 | rm -f cdk/package-lock.json 49 | endif 50 | 51 | clean-pyc: ## Remove Python file artifacts 52 | ifeq ($(OS),Windows_NT) 53 | Get-ChildItem * -Include *.pyc -Recurse | Remove-Item -ErrorAction SilentlyContinue; 54 | Get-ChildItem * -Include *.pyo -Recurse | Remove-Item -ErrorAction SilentlyContinue; 55 | else 56 | find . -name '*.pyc' -exec rm -f {} + 57 | find . -name '*.pyo' -exec rm -f {} + 58 | find . -name '*~' -exec rm -f {} + 59 | find . -name '__pycache__' -exec rm -fr {} + 60 | endif 61 | 62 | clean-test: ## Remove test and coverage artifacts 63 | ifeq ($(OS),Windows_NT) 64 | Get-ChildItem * -Include .tox* -Recurse | Remove-Item -ErrorAction SilentlyContinue; 65 | Get-ChildItem * -Include .coverage* -Recurse | Remove-Item -ErrorAction SilentlyContinue; 66 | Get-ChildItem * -Include htmlcov -Recurse | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue; 67 | Get-ChildItem * -Include .pytest_cache -Recurse | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue; 68 | else 69 | rm -fr .tox/ 70 | rm -f .coverage 71 | rm -fr htmlcov/ 72 | rm -fr .pytest_cache 73 | endif 74 | 75 | poetry-lock: load-codeartifact-settings 76 | ifeq ($(OS),Windows_NT) 77 | @.\scripts\poetry_pre_commit.ps1 -account $(account) -domain $(domain) -repoName $(repo_name) -region $(region) -command lock 78 | else 79 | @./scripts/poetry_pre-commit.sh -a $(account) -d $(domain) -r $(repo_name) -p lock 80 | endif 81 | 82 | lint: ## Autoformat with black check additional linting with flake8 83 | pre-commit run lint 84 | 85 | security: ## Security scans with pip-audit 86 | pip-audit 87 | pre-commit run security 88 | 89 | test: 90 | pytest -v 91 | pytest --cov=cdk 92 | 93 | pre-commit-all: 94 | pre-commit run --all-files 95 | 96 | synth: ## Run cdk synth 97 | cd cdk; cdk synth 98 | 99 | ls: ## Run cdk ls 100 | cd cdk; cdk ls 101 | 102 | diff: ## Run cdk diff 103 | cd cdk; cdk diff 104 | 105 | install: 106 | poetry install 107 | pre-commit install 108 | 109 | install-ci: 110 | poetry install 111 | 112 | load-pipeline-settings: 113 | $(eval QUALIFIER = $(shell cd cdk; python -c "from l3constructs.helpers.helper import Helper; import constants; helper = Helper(); print(helper.get_qualifier());")) 114 | $(eval APP = $(shell cd cdk; python -c "from l3constructs.helpers.helper import Helper; import constants; helper = Helper(); print(helper.get_cdk_app_name().capitalize());")) 115 | 116 | bootstrap: load-pipeline-settings ## Bootstrap tooling account 117 | @echo "Boostrapping for qualifier:$(QUALIFIER)" 118 | @cd cdk; cdk bootstrap --toolkit-stack-name "$(APP)BootstrapStack" --qualifier "$(QUALIFIER)" 119 | cleanup-bootstrap: load-pipeline-settings 120 | $(shell python ./scripts/cleanup_cdk_bootstrap.py $(QUALIFIER) "$(APP)BootstrapStack") 121 | 122 | deploy: load-pipeline-settings ## Deploy CDK pipeline 123 | @cd cdk; cdk deploy --progress events --require-approval never 124 | 125 | destroy: load-pipeline-settings ## Destroy CDK pipeline 126 | @cd cdk; cdk destroy --progress events --force 127 | -------------------------------------------------------------------------------- /app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import BytesIO 3 | from unittest import mock 4 | 5 | import pytest 6 | from attr import dataclass 7 | 8 | ACCOUNT_ID = "123456789012" 9 | REGION = "us-east-1" 10 | FINDING_ID = "test_finding_id" 11 | MEMBER_DETAILS = [{"AccountId": ACCOUNT_ID, "Email": "test@example.com"}] 12 | DETECTOR_ID = "test_detector_id" 13 | GRAPH_ARN = "test_graph_arn" 14 | S3_BUCKET_NAME = "test-bucket" 15 | TEST_FINDING = { 16 | "Findings": [ 17 | { 18 | "Id": FINDING_ID, 19 | "Severity": 6, 20 | "Type": "UnauthorizedAccess:EC2/SSHBruteForce", 21 | "AccountId": ACCOUNT_ID, 22 | "Region": REGION, 23 | } 24 | ] 25 | } 26 | SNS_TOPIC_ARN = "arn:aws:sns:us-east-1:123456789012:my-test-topic" 27 | EVENT = { 28 | "version": "0", 29 | "id": "sample-event-id", 30 | "detail-type": "GuardDuty Finding", 31 | "source": "aws.guardduty", 32 | "account": ACCOUNT_ID, 33 | "time": "2024-10-18T11:40:01Z", 34 | "region": REGION, 35 | "resources": [], 36 | "detail": { 37 | "schemaVersion": "2.0", 38 | "accountId": "123456789012", 39 | "region": "us-east-1", 40 | "id": FINDING_ID, 41 | "partition": "aws", 42 | "arn": f"arn:aws:guardduty:us-east-1:123456789012:detector/sample-detector/finding/{FINDING_ID}", 43 | "type": "UnauthorizedAccess:EC2/SSHBruteForce", 44 | "severity": 5.0, 45 | "service": { 46 | "serviceName": "guardduty", 47 | "detectorId": "sample-detector", 48 | "action": { 49 | "actionType": "NETWORK_CONNECTION", 50 | "networkConnectionAction": {"remoteIpDetails": {"ipAddressV4": "8.8.8.8"}}, 51 | }, 52 | }, 53 | "resource": { 54 | "resourceType": "Instance", 55 | "instanceDetails": {"instanceId": "i-1234567890abcdef0"}, 56 | }, 57 | }, 58 | } 59 | 60 | 61 | @pytest.fixture() 62 | def get_detective_list_graphs_response(): 63 | return {"GraphList": [{"Arn": GRAPH_ARN}], "NextToken": None} 64 | 65 | 66 | @pytest.fixture() 67 | def get_detective_list_members_response(): 68 | return {"MemberDetails": MEMBER_DETAILS, "NextToken": None} 69 | 70 | 71 | @pytest.fixture() 72 | def mock_detective_client(get_detective_list_graphs_response, get_detective_list_members_response): 73 | detective_client = mock.MagicMock(name="MockDetectiveClient") 74 | detective_client.list_graphs.return_value = get_detective_list_graphs_response 75 | detective_client.list_members.return_value = get_detective_list_members_response 76 | return detective_client 77 | 78 | 79 | @pytest.fixture() 80 | def mock_guardduty_client(): 81 | guardduty_client = mock.MagicMock(name="MockGuardDutyClient") 82 | guardduty_client.get_detector.return_value = {"DetectorId": DETECTOR_ID} 83 | guardduty_client.get_findings.return_value = TEST_FINDING 84 | return guardduty_client 85 | 86 | 87 | @pytest.fixture() 88 | def mock_s3_client(): 89 | s3_client = mock.MagicMock(name="MockS3Client") 90 | s3_client.create_bucket.return_value = {"Location": S3_BUCKET_NAME} 91 | s3_client.upload_fileobj.return_value = None 92 | s3_client.put_object.return_value = None 93 | return s3_client 94 | 95 | 96 | @pytest.fixture() 97 | def mock_sns_client(): 98 | sns_client = mock.MagicMock(name="MockSNSClient") 99 | sns_client.create_topic.return_value = {"TopicArn": SNS_TOPIC_ARN} 100 | sns_client.publish.return_value = {"MessageId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"} 101 | return sns_client 102 | 103 | 104 | @pytest.fixture() 105 | def get_bedrock_invoke_model_response(): 106 | return {"body": BytesIO(b'{"content": [{"text": "Test AI Insights"}]}')} 107 | 108 | 109 | @pytest.fixture() 110 | def mock_bedrock_client(get_bedrock_invoke_model_response): 111 | bedrock_client = mock.MagicMock(name="MockBedrockClient") 112 | bedrock_client.invoke_model.return_value = get_bedrock_invoke_model_response 113 | return bedrock_client 114 | 115 | 116 | @pytest.fixture() 117 | def mock_events_client(): 118 | events_client = mock.MagicMock(name="MockEventsClient") 119 | return events_client 120 | 121 | 122 | # Generates a mock lambda context 123 | @pytest.fixture(autouse=True) 124 | def lambda_context(): 125 | @dataclass 126 | class context: 127 | function_name = "test" 128 | memory_limit_in_mb = 128 129 | invoked_function_arn = f"arn:aws:lambda:{REGION}:{ACCOUNT_ID}:function:test" 130 | aws_request_id = "52fdfc07-2182-154f-163f-5f0f9a621d72" 131 | 132 | return context 133 | 134 | 135 | # Generate mock environment variables for testing 136 | @pytest.fixture(autouse=True) 137 | def aws_credentials(): 138 | """Mocked AWS Credentials for moto.""" 139 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 140 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 141 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 142 | os.environ["AWS_SESSION_TOKEN"] = "testing" 143 | os.environ["AWS_REGION"] = REGION 144 | os.environ["AWS_DEFAULT_REGION"] = REGION 145 | os.environ["AWS_ACCOUNT"] = ACCOUNT_ID 146 | os.environ["POWERTOOLS_METRICS_NAMESPACE"] = "Test" 147 | os.environ["POWERTOOLS_SERVICE_NAME"] = "ProductPackaging" 148 | os.environ["LOG_LEVEL"] = "DEBUG" 149 | os.environ["AI_REPORTS_TOPIC_ARN"] = SNS_TOPIC_ARN 150 | os.environ["AI_REPORTS_BUCKET_NAME"] = S3_BUCKET_NAME 151 | os.environ["BEDROCK_MODEL_ID"] = "anthropic.claude-3-5-sonnet-20240620-v1:0" 152 | os.environ["body_args_anthropic_version"] = "dummy" 153 | 154 | 155 | @pytest.fixture() 156 | def get_guardduty_event(): 157 | return EVENT 158 | -------------------------------------------------------------------------------- /cdk/l3constructs/lambda_functions/L3LambdaPython.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import platform 4 | import re 5 | import subprocess 6 | from typing import List, Mapping, Optional 7 | 8 | import aws_cdk 9 | from aws_cdk import Duration, Stack 10 | from aws_cdk import aws_iam as iam 11 | from aws_cdk import aws_kms, aws_lambda 12 | from constructs import Construct 13 | from jsii import implements, member 14 | 15 | from .L3Lambda import L3Lambda 16 | 17 | # Set up logging 18 | logging.basicConfig(level=logging.INFO) 19 | 20 | 21 | @implements(aws_cdk.ILocalBundling) 22 | class MyLocalBundler: 23 | def __init__(self, lambda_root: str, app_root: str) -> None: 24 | self._lambda_root = lambda_root 25 | self._app_root = app_root 26 | 27 | @member(jsii_name="tryBundle") 28 | def try_bundle(self, output_dir: str, options: aws_cdk.BundlingOptions) -> bool: 29 | try: 30 | target_dir = os.path.join(output_dir) 31 | source_dir = os.path.abspath(self._lambda_root) 32 | 33 | # Create commands list based on OS 34 | if platform.system() == "Windows": 35 | commands = [ 36 | [ 37 | "pip", 38 | "install", 39 | "-r", 40 | os.path.join(self._lambda_root, "requirements.txt"), 41 | "-t", 42 | target_dir, 43 | ], 44 | ["xcopy", "/E", "/I", "/Y", source_dir, target_dir], 45 | ] 46 | else: 47 | commands = [ 48 | [ 49 | "pip", 50 | "install", 51 | "-U", 52 | "-r", 53 | os.path.join(self._lambda_root, "requirements.txt"), 54 | "-t", 55 | target_dir, 56 | ], 57 | ["rsync", "-a", f"{source_dir}/", f"{target_dir}/"], 58 | ] 59 | 60 | # Execute commands using list format (more secure than shell=True) 61 | for cmd in commands: 62 | result = subprocess.run( 63 | cmd, 64 | capture_output=True, 65 | text=True, 66 | check=False, 67 | ) 68 | logging.info(f"Result: {result}") 69 | if result.returncode != 0: 70 | logging.error(f"Command failed: {' '.join(cmd)}") 71 | logging.error(f"Error output: {result.stderr}") 72 | return False 73 | logging.info(f"Command output: {result.stdout}") 74 | 75 | logging.info("Bundling completed successfully") 76 | return True 77 | 78 | except Exception as e: 79 | logging.error(f"Error during bundling: {str(e)}") 80 | return False 81 | 82 | 83 | class L3LambdaPython(L3Lambda): 84 | def __init__( 85 | self, 86 | scope: Construct, 87 | id: str, 88 | code: str, 89 | handler: str, 90 | runtime: aws_lambda.Runtime = aws_lambda.Runtime.PYTHON_3_12, 91 | role: Optional[iam.Role] = None, 92 | vpc: Optional[str] = None, 93 | environment: Optional[Mapping] = None, 94 | environment_encryption: Optional[aws_kms.Key] = None, 95 | security_groups: Optional[list] = None, 96 | layers: Optional[List[aws_lambda.LayerVersion]] = None, 97 | description: Optional[str] = None, 98 | memory: int = 128, 99 | timeout: Duration = Duration.seconds(60), 100 | ): 101 | super(L3LambdaPython, self).__init__( 102 | scope=scope, 103 | id=id, 104 | code=L3LambdaPython.bundle_locally(app_root="", lambda_root=code, runtime=runtime), 105 | handler=handler, 106 | runtime=runtime or None, 107 | role=role or None, 108 | vpc=vpc or None, 109 | description=description or None, 110 | layers=layers or None, 111 | environment=environment or None, 112 | environment_encryption=environment_encryption or None, 113 | security_groups=security_groups or None, 114 | timeout=timeout, 115 | memory_size=memory, 116 | ) 117 | 118 | @staticmethod 119 | def format_dockerfile( 120 | scope: Construct, 121 | id: str, 122 | code: str, 123 | runtime: aws_lambda.Runtime, 124 | docker_file: str = "Dockerfile.python", 125 | ) -> aws_lambda.Code: 126 | input_docker_file_path = f"./cdk/l3_constructs/lambda_functions/docker_files/{docker_file}" 127 | output_docker_file_name = f"Dockerfile.{Stack.of(scope).stack_name}.{id}" 128 | output_docker_file_path = f"{code}/{output_docker_file_name}" 129 | 130 | with open(input_docker_file_path, "r") as docker_file_r: 131 | file = docker_file_r.read() 132 | file_content = file.format( 133 | **{"RUNTIME": re.sub(r"([a-z_-]+)", r"\g<1>:", runtime.to_string(), 1)} 134 | ) 135 | 136 | with open(output_docker_file_path, "w") as docker_file_w: 137 | docker_file_w.write(file_content) 138 | 139 | docker_build = aws_lambda.Code.from_docker_build( 140 | path=os.path.abspath(code), file=output_docker_file_name, platform="linux/amd64" 141 | ) 142 | 143 | return docker_build 144 | 145 | @staticmethod 146 | def bundle_locally(app_root: str, lambda_root: str, runtime: aws_lambda.Runtime): 147 | hash_path = lambda_root 148 | 149 | asset_hash = aws_cdk.FileSystem.fingerprint(hash_path) 150 | 151 | code = aws_lambda.Code.from_asset( 152 | path=lambda_root, 153 | bundling=aws_cdk.BundlingOptions( 154 | image=runtime.bundling_image, 155 | command=["bash", "-c", f"rsync -r {lambda_root} /asset-output/{app_root}/"], 156 | local=MyLocalBundler(lambda_root=lambda_root, app_root=app_root), 157 | ), 158 | asset_hash=asset_hash, 159 | asset_hash_type=aws_cdk.AssetHashType.CUSTOM, 160 | ) 161 | return code 162 | -------------------------------------------------------------------------------- /cdk/tests/test_ai_security_recommendations_stack.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from aws_cdk import App, assertions 3 | from l3constructs.helpers.helper import Helper 4 | from stacks.ai_security_recommendations import AISecurityRecommendations 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def load_stack(): 9 | app = App() 10 | Helper(tags={}, prefix="") 11 | stack = AISecurityRecommendations(app, "AISecurityRecommendations", env={"region": "us-east-1"}) 12 | return assertions.Template.from_stack(stack) 13 | 14 | 15 | # Test to check if the S3 bucket is created 16 | def test_s3_bucket_created(load_stack): 17 | load_stack.has_resource_properties( 18 | "AWS::S3::Bucket", 19 | { 20 | "BucketEncryption": assertions.Match.any_value(), 21 | "LoggingConfiguration": assertions.Match.object_like( 22 | {"DestinationBucketName": assertions.Match.any_value()} 23 | ), 24 | "PublicAccessBlockConfiguration": assertions.Match.any_value(), 25 | "VersioningConfiguration": assertions.Match.any_value(), 26 | }, 27 | ) 28 | 29 | 30 | # Test to verify if SNS Topic is created with encryption enabled 31 | def test_sns_topic_created_with_encryption(load_stack): 32 | load_stack.has_resource_properties( 33 | "AWS::SNS::Topic", 34 | { 35 | "TopicName": "AiSecurityRecommendationsTopic", 36 | "KmsMasterKeyId": assertions.Match.any_value(), 37 | }, 38 | ) 39 | 40 | 41 | # Test to check if the EventBridge Rule is created for GuardDuty events 42 | def test_event_rule_created(load_stack): 43 | load_stack.has_resource_properties( 44 | "AWS::Events::Rule", 45 | {"EventPattern": {"source": ["aws.guardduty"], "detail-type": ["GuardDuty Finding"]}}, 46 | ) 47 | 48 | 49 | # Test to check if Lambda function is created with correct environment variables 50 | def test_lambda_function_created(load_stack): 51 | load_stack.has_resource_properties( 52 | "AWS::Lambda::Function", 53 | { 54 | "Handler": "index.lambda_handler", 55 | "Environment": { 56 | "Variables": { 57 | "AI_REPORTS_BUCKET_NAME": assertions.Match.any_value(), 58 | "AI_REPORTS_TOPIC_ARN": assertions.Match.any_value(), 59 | "BEDROCK_MODEL_ID": assertions.Match.any_value(), 60 | } 61 | }, 62 | }, 63 | ) 64 | 65 | 66 | # Test to check if Lambda function has S3, SNS, and GuardDuty permissions 67 | def test_lambda_has_required_permissions(load_stack): 68 | expected_policy = { 69 | "Statement": [ 70 | { 71 | "Action": [ 72 | "s3:DeleteObject*", 73 | "s3:PutObject", 74 | "s3:PutObjectLegalHold", 75 | "s3:PutObjectRetention", 76 | "s3:PutObjectTagging", 77 | "s3:PutObjectVersionTagging", 78 | "s3:Abort*", 79 | ], 80 | "Effect": "Allow", 81 | "Resource": assertions.Match.any_value(), 82 | }, 83 | { 84 | "Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"], 85 | "Effect": "Allow", 86 | "Resource": assertions.Match.any_value(), 87 | }, 88 | { 89 | "Action": ["kms:Decrypt", "kms:Encrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*"], 90 | "Effect": "Allow", 91 | "Resource": assertions.Match.any_value(), 92 | }, 93 | {"Action": "sns:Publish", "Effect": "Allow", "Resource": assertions.Match.any_value()}, 94 | { 95 | "Action": [ 96 | "guardduty:GetFindings", 97 | "guardduty:ListDetectors", 98 | "guardduty:ListFindings", 99 | ], 100 | "Effect": "Allow", 101 | "Resource": "arn:aws:guardduty:*:*:detector/*", 102 | }, 103 | { 104 | "Action": [ 105 | "detective:ListGraphs", 106 | "detective:GetMembers", 107 | "detective:ListDatasourcePackages", 108 | "detective:ListActivities", 109 | "detective:ListMembers", 110 | ], 111 | "Effect": "Allow", 112 | "Resource": "*", 113 | }, 114 | { 115 | "Action": "bedrock:InvokeModel", 116 | "Effect": "Allow", 117 | "Resource": assertions.Match.any_value(), 118 | }, 119 | { 120 | "Action": [ 121 | "events:PutRule", 122 | "events:PutTargets", 123 | "events:DeleteRule", 124 | "events:RemoveTargets", 125 | ], 126 | "Effect": "Allow", 127 | "Resource": assertions.Match.any_value(), 128 | }, 129 | ], 130 | "Version": "2012-10-17", 131 | } 132 | 133 | load_stack.has_resource_properties( 134 | "AWS::IAM::Policy", 135 | { 136 | "PolicyDocument": expected_policy, 137 | "PolicyName": "LambdaExecutionRoleDefaultPolicy6D69732F", 138 | "Roles": [{"Ref": "LambdaExecutionRoleD5C26073"}], 139 | }, 140 | ) 141 | 142 | 143 | # Test for EventBridge rescheduling permissions in Lambda IAM policy 144 | def test_lambda_rescheduling_permissions(load_stack): 145 | load_stack.has_resource_properties( 146 | "AWS::IAM::Policy", 147 | { 148 | "PolicyDocument": { 149 | "Statement": assertions.Match.array_with( 150 | [ 151 | assertions.Match.object_like( 152 | { 153 | "Action": [ 154 | "events:PutRule", 155 | "events:PutTargets", 156 | "events:DeleteRule", 157 | "events:RemoveTargets", 158 | ], 159 | "Effect": "Allow", 160 | "Resource": assertions.Match.any_value(), 161 | } 162 | ) 163 | ] 164 | ) 165 | } 166 | }, 167 | ) 168 | 169 | 170 | # Test if KMS key is created with key rotation enabled 171 | def test_kms_key_with_rotation(load_stack): 172 | load_stack.has_resource_properties("AWS::KMS::Key", {"EnableKeyRotation": True}) 173 | -------------------------------------------------------------------------------- /cdk/l3constructs/helpers/helper.py: -------------------------------------------------------------------------------- 1 | # pylint: disable = W1514 2 | """ 3 | Helper module to be used in other projects to 4 | e.g. generate qualifiers or retrieve repository information 5 | """ 6 | 7 | import hashlib 8 | import json 9 | import os 10 | from pathlib import Path 11 | 12 | import cdk_nag 13 | from pygit2 import Repository 14 | 15 | 16 | class Helper: 17 | """ 18 | Helper that supports the customization of CDK l3constructs. 19 | """ 20 | 21 | def __init__( 22 | self, 23 | cdk_env: str = None, 24 | qualifier: str = None, 25 | repo_name: str = None, 26 | branch_name: str = None, 27 | prefix: str = None, 28 | tags: dict = None, 29 | ): 30 | self.cdk_env = cdk_env 31 | self.qualifier = qualifier 32 | self.repo_name = repo_name 33 | self.branch_name = branch_name 34 | self.prefix = prefix 35 | self.tags = tags 36 | 37 | @staticmethod 38 | def get_repo_name_from_url(url: str) -> str: 39 | """ 40 | Retrieve repository name from repository url 41 | 42 | :param url: git branch url 43 | :return: repository name 44 | """ 45 | last_slash_index = max(url.rfind("/"), url.rfind("@")) 46 | last_suffix_index = url.rfind(".git") 47 | if last_suffix_index < 0: 48 | last_suffix_index = len(url) 49 | 50 | if last_slash_index < 0 or last_suffix_index <= last_slash_index: 51 | raise Exception(f"Badly formatted url {url}") 52 | 53 | return url[last_slash_index + 1 : last_suffix_index] 54 | 55 | def get_repo_name_from_local_git(self): 56 | """ 57 | Retrieve repository name from locally selected git branch 58 | 59 | :return: repository name 60 | """ 61 | if self.repo_name is None: 62 | if "REPO_NAME" in os.environ: 63 | self.repo_name = os.environ["REPO_NAME"] 64 | else: 65 | repo = Repository(os.getcwd()) 66 | self.repo_name = self.get_repo_name_from_url(repo.remotes[0].url) 67 | return self.repo_name 68 | 69 | def get_branch_name_from_local_git(self) -> str: 70 | """ 71 | Retrieve git branch name from currently locally selected 72 | branch 73 | 74 | :return: string git branch 75 | """ 76 | if self.branch_name is None: 77 | if "BRANCH_NAME" in os.environ: 78 | self.branch_name = os.environ["BRANCH_NAME"] 79 | else: 80 | repo = Repository(os.getcwd()) 81 | self.branch_name = repo.head.shorthand 82 | return self.branch_name 83 | 84 | def get_cdk_env(self): 85 | """ 86 | Get cdk env to check the environment 87 | 88 | :return: environment: str 89 | """ 90 | if self.cdk_env is None: 91 | if "CDK_ENV" in os.environ: 92 | self.cdk_env = os.environ["CDK_ENV"] 93 | else: 94 | self.repo_name = self.get_repo_name() 95 | self.branch_name = self.get_repo_branch() 96 | 97 | return self.cdk_env 98 | 99 | def calculate_qualifier(self) -> str: 100 | """ 101 | Helper function to calculate hashed app name to be used as qualifier 102 | 103 | :return: hashed_name: str 104 | """ 105 | if self.qualifier is None: 106 | if "QUALIFIER" in os.environ: 107 | self.qualifier = os.environ["QUALIFIER"] 108 | else: 109 | app_name = f"{self.get_cdk_app_name()}_{self.branch_name}" 110 | hashed_name = hashlib.sha256(app_name.encode()).hexdigest()[:30] 111 | index_first_non_numeric = hashed_name.find(next(filter(str.isalpha, hashed_name))) 112 | self.qualifier = hashed_name[ 113 | index_first_non_numeric : (10 + index_first_non_numeric) 114 | ] 115 | return self.qualifier 116 | 117 | def append_qualifier(self, name: str) -> str: 118 | """ 119 | Append qualifier to logical id to avoid naming 120 | conflicts, due to multi environment setup 121 | 122 | :param name: logical id of CDK resource 123 | :return: logical id with qualifier prefix: str 124 | """ 125 | if self.qualifier is None: 126 | self.calculate_qualifier() 127 | return f"{self.qualifier}-{name}" 128 | 129 | def get_repo_branch(self) -> str: 130 | """ 131 | TODO: Update 132 | Set repo branch name my removing non-numeric values from qualifier 133 | Checks if qualifier in reserved, if so use main branch 134 | Replace once automation in place, that creates pipelines on branch creation 135 | """ 136 | if self.branch_name is None: 137 | self.get_branch_name_from_local_git() 138 | return self.branch_name 139 | 140 | def get_repo_name(self) -> str: 141 | """ 142 | TODO: Update 143 | Set repo branch name my removing non-numeric values from qualifier 144 | Checks if qualifier in reserved, if so use main branch 145 | Replace once automation in place, that creates pipelines on branch creation 146 | """ 147 | if self.repo_name is None: 148 | self.get_repo_name_from_local_git() 149 | return self.repo_name 150 | 151 | @staticmethod 152 | def find_cdk(name: str, path: str): 153 | """ 154 | Find a file in a given path 155 | :param name: file name to look for 156 | :param path: path in which to look for the file 157 | :return: returns path of the file location 158 | """ 159 | for root, dirs, files in os.walk(path): 160 | if name in files: 161 | return os.path.join(root, name) 162 | 163 | def read_cdk_context_json(self) -> dict: 164 | """ 165 | Read cdk.json context file 166 | 167 | :return: dict of objects read from cdk.json: dict 168 | """ 169 | try: 170 | file_path = self.find_cdk("cdk.json", Path(__file__).absolute().parent.parent.parent) 171 | except FileNotFoundError: 172 | print("File was not found") 173 | 174 | with open(file_path, "r") as my_file: 175 | data = my_file.read() 176 | 177 | obj = json.loads(data) 178 | return obj 179 | 180 | def get_cdk_app_name(self) -> str: 181 | """ 182 | Read cdk app name from cdk context 183 | 184 | :return: cdk app name: str 185 | """ 186 | cdk_context = self.read_cdk_context_json() 187 | app_name = cdk_context.get("context").get("app-name") 188 | return app_name 189 | 190 | def get_qualifier(self) -> str: 191 | """ 192 | Retrieve qualifier for this setup 193 | 194 | :return: qualifier: str 195 | """ 196 | if self.qualifier is None: 197 | self.qualifier = self.calculate_qualifier() 198 | return self.qualifier 199 | 200 | def cdk_nag_add_resource_suppression( 201 | self, resource: any, suppression_id: str, reason: str = None, apply_to_children: bool = True 202 | ): 203 | """ 204 | 205 | :param resource: cdk resource to apply the suppression 206 | :param suppression_id: cdk suppression id 207 | :param reason: reason for the suppression 208 | :param apply_to_children: apply to all children under the resource 209 | """ 210 | return cdk_nag.NagSuppressions.add_resource_suppressions( 211 | resource, 212 | suppressions=[ 213 | { 214 | "id": suppression_id, 215 | "reason": reason or "Default suppression by the helper, message not provided", 216 | } 217 | ], 218 | apply_to_children=apply_to_children, 219 | ) 220 | -------------------------------------------------------------------------------- /app/tests/test_ai_generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest import mock 3 | 4 | import assertpy 5 | import pytest 6 | from botocore.exceptions import ClientError 7 | 8 | from app.ai_generator import index 9 | from app.ai_generator.index import ( 10 | generate_pdf, 11 | get_all_detective_entities, 12 | get_graph_arn, 13 | get_guardduty_finding, 14 | invoke_bedrock_model, 15 | send_sns_notification, 16 | ) 17 | from app.tests.conftest import ( 18 | DETECTOR_ID, 19 | FINDING_ID, 20 | GRAPH_ARN, 21 | MEMBER_DETAILS, 22 | SNS_TOPIC_ARN, 23 | TEST_FINDING, 24 | ) 25 | 26 | 27 | def test_get_guardduty_finding(mock_guardduty_client): 28 | finding = get_guardduty_finding(mock_guardduty_client, DETECTOR_ID, FINDING_ID) 29 | assertpy.assert_that(finding).is_not_none() 30 | assertpy.assert_that(finding["Id"]).is_equal_to(FINDING_ID) 31 | 32 | 33 | def test_get_graph_arn(mock_detective_client): 34 | graph_arn = get_graph_arn(mock_detective_client) 35 | assertpy.assert_that(graph_arn).is_not_none() 36 | assertpy.assert_that(graph_arn).is_equal_to(GRAPH_ARN) 37 | 38 | 39 | def test_get_all_detective_entities(mock_detective_client): 40 | entities = get_all_detective_entities(mock_detective_client, GRAPH_ARN) 41 | assertpy.assert_that(entities).is_not_empty() 42 | assertpy.assert_that(entities).is_equal_to(MEMBER_DETAILS) 43 | 44 | 45 | def test_invoke_bedrock_model(mock_bedrock_client, lambda_context, get_guardduty_event): 46 | ai_insights = invoke_bedrock_model( 47 | mock_bedrock_client, TEST_FINDING, DETECTOR_ID, get_guardduty_event, lambda_context 48 | ) 49 | assertpy.assert_that(ai_insights).is_not_none() 50 | assertpy.assert_that(ai_insights).is_equal_to("Test AI Insights") 51 | 52 | 53 | def test_generate_pdf(): 54 | ai_insights = "Test AI Insights" 55 | guardduty_finding = { 56 | "Id": FINDING_ID, 57 | "Severity": 6, 58 | "Type": "UnauthorizedAccess:EC2/SSHBruteForce", 59 | } 60 | pdf_buffer = generate_pdf(ai_insights, guardduty_finding) 61 | assertpy.assert_that(pdf_buffer).is_not_none() 62 | assertpy.assert_that(len(pdf_buffer.getvalue())).is_greater_than(0) 63 | 64 | 65 | def test_sns_notification(mock_sns_client): 66 | pre_signed_url = "https://example.com/pre-signed-url" 67 | pre_signed_url2 = pre_signed_url.strip().replace(" ", "%20") 68 | message = { 69 | "default": "New GuardDuty finding with enriched PDF. Click the link to download.", 70 | "email": ( 71 | "New GuardDuty finding. Enriched PDF with AI remediation's can be downloaded here: " 72 | f"{pre_signed_url2}" 73 | ), 74 | } 75 | send_sns_notification(mock_sns_client, pre_signed_url, SNS_TOPIC_ARN) 76 | mock_sns_client.publish.assert_called_once_with( 77 | TopicArn=SNS_TOPIC_ARN, 78 | Message=json.dumps(message), 79 | Subject="GuardDuty Finding Enrichment Report", 80 | MessageStructure="json", 81 | ) 82 | 83 | 84 | # Configure boto3 client mock to return the correct client based on input 85 | @pytest.fixture 86 | def boto_client_side_effect(): 87 | # Create a unique mock for each AWS service client 88 | # Create a unique mock for each AWS service client with a specified name 89 | mock_sns_client = mock.Mock(name="MockSNSClient") 90 | mock_guardduty_client = mock.Mock(name="MockGuardDutyClient") 91 | mock_detective_client = mock.Mock(name="MockDetectiveClient") 92 | mock_bedrock_client = mock.Mock(name="MockBedrockClient") 93 | mock_s3_client = mock.Mock(name="MockS3Client") 94 | mock_events_client = mock.Mock(name="MockEventsClient") 95 | 96 | # Define the side effect function 97 | def client_side_effect(service_name, *args, **kwargs): 98 | if service_name == "sns": 99 | return mock_sns_client 100 | elif service_name == "guardduty": 101 | return mock_guardduty_client 102 | elif service_name == "detective": 103 | return mock_detective_client 104 | elif service_name == "bedrock-runtime": 105 | return mock_bedrock_client 106 | elif service_name == "s3": 107 | return mock_s3_client 108 | elif service_name == "events": 109 | return mock_events_client 110 | else: 111 | raise ValueError(f"Unexpected service: {service_name}") 112 | 113 | # Return the side effect function and each mock client for test configuration 114 | return client_side_effect, { 115 | "sns": mock_sns_client, 116 | "guardduty": mock_guardduty_client, 117 | "detective": mock_detective_client, 118 | "bedrock": mock_bedrock_client, 119 | "s3": mock_s3_client, 120 | "events": mock_events_client, 121 | } 122 | 123 | 124 | @mock.patch("boto3.client") 125 | def test_lambda_handler( 126 | mock_boto_client, 127 | boto_client_side_effect, 128 | lambda_context, 129 | get_bedrock_invoke_model_response, 130 | get_detective_list_graphs_response, 131 | get_detective_list_members_response, 132 | get_guardduty_event, 133 | ): 134 | # Mock return values for each AWS service client 135 | client_side_effect, mock_clients = boto_client_side_effect 136 | mock_boto_client.side_effect = client_side_effect 137 | 138 | mock_clients["guardduty"].get_findings.return_value = TEST_FINDING 139 | mock_clients["detective"].list_members.return_value = get_detective_list_members_response 140 | mock_clients["detective"].list_graphs.return_value = get_detective_list_graphs_response 141 | mock_clients["bedrock"].invoke_model.return_value = get_bedrock_invoke_model_response 142 | mock_clients["s3"].put_object.return_value = None 143 | mock_clients["s3"].generate_presigned_url.return_value = "https://dummyurl" 144 | mock_clients["sns"].publish.return_value = None 145 | response = index.lambda_handler(get_guardduty_event, lambda_context) 146 | assertpy.assert_that(response).is_not_none() 147 | assertpy.assert_that(response["statusCode"]).is_equal_to(200) 148 | assertpy.assert_that(json.loads(response["body"])).is_equal_to("Test AI Insights") 149 | mock_clients["guardduty"].get_findings.assert_called_once() 150 | mock_clients["detective"].list_members.assert_called_once() 151 | mock_clients["detective"].list_graphs.assert_called_once() 152 | mock_clients["bedrock"].invoke_model.assert_called_once() 153 | mock_clients["s3"].put_object.assert_called_once() 154 | mock_clients["s3"].generate_presigned_url.assert_called_once() 155 | mock_clients["sns"].publish.assert_called_once() 156 | 157 | 158 | @mock.patch("boto3.client") 159 | def test_lambda_handler_bedrock_throttling( 160 | mock_boto_client, 161 | boto_client_side_effect, 162 | lambda_context, 163 | get_bedrock_invoke_model_response, 164 | get_detective_list_graphs_response, 165 | get_detective_list_members_response, 166 | get_guardduty_event, 167 | ): 168 | client_side_effect, mock_clients = boto_client_side_effect 169 | mock_boto_client.side_effect = client_side_effect 170 | 171 | mock_clients["guardduty"].get_findings.return_value = TEST_FINDING 172 | mock_clients["detective"].list_members.return_value = get_detective_list_members_response 173 | mock_clients["detective"].list_graphs.return_value = get_detective_list_graphs_response 174 | mock_clients["bedrock"].invoke_model.side_effect = ClientError( 175 | {"Error": {"Code": "ThrottlingException", "Message": "Rate exceeded"}}, "InvokeModel" 176 | ) 177 | mock_clients["events"].put_events.return_value = None 178 | response = index.lambda_handler(get_guardduty_event, lambda_context) 179 | mock_clients["guardduty"].get_findings.assert_called_once() 180 | mock_clients["detective"].list_members.assert_called_once() 181 | mock_clients["detective"].list_graphs.assert_called_once() 182 | mock_clients["bedrock"].invoke_model.assert_called_once() 183 | assertpy.assert_that(response).is_not_none() 184 | assertpy.assert_that(response["statusCode"]).is_equal_to(200) 185 | assertpy.assert_that(json.loads(response["body"])).is_equal_to("Event scheduled.") 186 | -------------------------------------------------------------------------------- /cdk/stacks/ai_security_recommendations.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cdk_nag 4 | import constants 5 | from aws_cdk import aws_events as events 6 | from aws_cdk import aws_events_targets as targets 7 | from aws_cdk import aws_iam as iam 8 | from aws_cdk import aws_kms as kms 9 | from aws_cdk import aws_sns as sns 10 | from constructs import Construct 11 | from l3constructs.helpers.base_stack import BaseStack 12 | from l3constructs.lambda_functions.L3LambdaPython import L3LambdaPython 13 | from l3constructs.s3.l3_bucket import L3S3Bucket 14 | 15 | 16 | class AISecurityRecommendations(BaseStack): 17 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 18 | super().__init__(scope, construct_id, **kwargs) 19 | 20 | # Instantiate the L3 S3 bucket with loggin enabled 21 | bucket_construct = L3S3Bucket( 22 | self, 23 | "AiReportsBucketConstruct", 24 | bucket_name="AiReportsBucket", 25 | log_bucket_name="AiReportsLogBucket", 26 | ) 27 | 28 | # Access the bucket and log bucket if needed 29 | self.reports_bucket = bucket_construct.get_bucket() 30 | self.log_bucket = bucket_construct.get_log_bucket() 31 | 32 | # create the SNS topic for the notifications 33 | # Create a KMS key for encryption 34 | self.topic_encryption_key = kms.Key( 35 | self, f"{id}Key", enable_key_rotation=True # Enable key rotation for added security 36 | ) 37 | self.topic = sns.Topic( 38 | self, 39 | "AiSecurityRecommendationsTopic", 40 | topic_name="AiSecurityRecommendationsTopic", 41 | display_name="AI Security Recommendations Topic", 42 | enforce_ssl=True, 43 | master_key=self.topic_encryption_key, 44 | ) 45 | 46 | # event bridge rule to trigger the lambda based on guardduty event 47 | self.rule = events.Rule( 48 | self, 49 | "AiSecurityRecommendationsRule", 50 | event_pattern=events.EventPattern( 51 | source=["aws.guardduty"], detail_type=["GuardDuty Finding"] 52 | ), 53 | ) 54 | 55 | # Create a custom IAM role for the Lambda function 56 | self.lambda_role = iam.Role( 57 | self, "LambdaExecutionRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com") 58 | ) 59 | 60 | # Create the Lambda function using the L3 construct 61 | self.lambda_function = L3LambdaPython( 62 | self, 63 | "L3LambdaPython", 64 | code=os.path.join( 65 | os.path.dirname(__file__), "..", "..", "app", "ai_generator" 66 | ), # Path to Lambda code 67 | handler="index.lambda_handler", # Lambda entry point 68 | role=self.lambda_role, # Set the custom role 69 | # layers=[constants.LAMBDA_PILLOW_LAYER], 70 | environment={ 71 | "AI_REPORTS_BUCKET_NAME": self.reports_bucket.bucket_name, 72 | "AI_REPORTS_TOPIC_ARN": self.topic.topic_arn, 73 | "BEDROCK_MODEL_ID": constants.BEDROCK_MODEL_ID, 74 | "body_args_anthropic_version": constants.ANTROPIC_VERSION, 75 | }, 76 | ) 77 | 78 | # # Grant write access to the Lambda function for the S3 bucket 79 | self.reports_bucket.grant_write(self.lambda_function.role) 80 | self.lambda_function.role.add_managed_policy( 81 | iam.ManagedPolicy.from_aws_managed_policy_name( 82 | "service-role/AWSLambdaBasicExecutionRole" 83 | ) 84 | ) 85 | 86 | # add cdk_nag exclusion for lambda role resource 87 | # Suppress the AWS managed policy warning 88 | cdk_nag.NagSuppressions.add_resource_suppressions( 89 | self.lambda_role, 90 | [ 91 | cdk_nag.NagPackSuppression( 92 | id="AwsSolutions-IAM4", 93 | reason="Managed Policies are for service account roles only", 94 | ) 95 | ], 96 | apply_to_children=True, 97 | ) 98 | 99 | cdk_nag.NagSuppressions.add_resource_suppressions( 100 | self.lambda_role, 101 | [ 102 | cdk_nag.NagPackSuppression( 103 | id="AwsSolutions-IAM5", 104 | reason="Managed Policies are for service account roles only", 105 | ) 106 | ], 107 | apply_to_children=True, 108 | ) 109 | 110 | # Grant write, read access to the bucket to store the pdf and generate the pre-signed link 111 | self.lambda_function.role.add_to_policy( 112 | iam.PolicyStatement( 113 | effect=iam.Effect.ALLOW, 114 | actions=["s3:PutObject", "s3:GetObject", "s3:ListBucket"], 115 | resources=[self.reports_bucket.bucket_arn, self.reports_bucket.bucket_arn + "/*"], 116 | ) 117 | ) 118 | 119 | # Grant publish access to the SNS topic 120 | # self.topic.grant_publish(self.lambda_function.role) permissions are to wide for this 121 | self.topic.add_to_resource_policy( 122 | iam.PolicyStatement( 123 | effect=iam.Effect.ALLOW, 124 | principals=[iam.ServicePrincipal("lambda.amazonaws.com")], 125 | actions=["sns:Publish"], 126 | resources=[self.topic.topic_arn], 127 | conditions={"ArnEquals": {"aws:SourceArn": self.lambda_function.function_arn}}, 128 | ) 129 | ) 130 | self.topic_encryption_key.grant_encrypt_decrypt(self.lambda_function.role) 131 | 132 | # Grant sns Publish access to the lambda function role 133 | self.lambda_function.role.add_to_policy( 134 | iam.PolicyStatement( 135 | effect=iam.Effect.ALLOW, actions=["sns:Publish"], resources=[self.topic.topic_arn] 136 | ) 137 | ) 138 | 139 | # Grant Guardduty access to the lambda function role 140 | self.lambda_function.role.add_to_policy( 141 | iam.PolicyStatement( 142 | effect=iam.Effect.ALLOW, 143 | actions=[ 144 | "guardduty:GetFindings", 145 | "guardduty:ListDetectors", 146 | "guardduty:ListFindings", 147 | ], 148 | resources=["arn:aws:guardduty:*:*:detector/*"], 149 | ) 150 | ) 151 | # Grant Permissions to query detective 152 | self.lambda_function.role.add_to_policy( 153 | iam.PolicyStatement( 154 | effect=iam.Effect.ALLOW, 155 | actions=[ 156 | "detective:ListGraphs", 157 | "detective:GetMembers", 158 | "detective:ListDatasourcePackages", 159 | "detective:ListActivities", 160 | "detective:ListMembers", 161 | ], 162 | resources=["*"], 163 | ) 164 | ) 165 | # lets grant permission to invoke the bedrock model 166 | self.lambda_function.role.add_to_policy( 167 | iam.PolicyStatement( 168 | effect=iam.Effect.ALLOW, 169 | actions=["bedrock:InvokeModel"], 170 | resources=[ 171 | f"arn:aws:bedrock:{self.region}::model/{constants.BEDROCK_MODEL_ID}", 172 | f"arn:aws:bedrock:{self.region}::foundation-model/{constants.BEDROCK_MODEL_ID}", 173 | ], 174 | ) 175 | ) 176 | # grant permissions to reschedule an event in case of throttling 177 | self.lambda_function.role.add_to_policy( 178 | iam.PolicyStatement( 179 | effect=iam.Effect.ALLOW, 180 | actions=[ 181 | "events:PutRule", 182 | "events:PutTargets", 183 | "events:DeleteRule", 184 | "events:RemoveTargets", 185 | ], 186 | resources=[ 187 | f"arn:aws:events:{self.region}:{self.account}:rule/RetryLambdaInvocation-*" 188 | ], 189 | ) 190 | ) 191 | # grant invoke to sns event to trigger lambda function from resource arn:aws:events:*:{self.account}:rule/RetryLambdaInvocation-* 192 | # Add permission for the Event Rule to invoke the Lambda function 193 | self.lambda_function.add_permission( 194 | "RetryEventsPermissions", 195 | principal=iam.ServicePrincipal("events.amazonaws.com"), 196 | action="lambda:InvokeFunction", 197 | source_arn=f"arn:aws:events:{self.region}:{self.account}:rule/RetryLambdaInvocation-*", 198 | ) 199 | # lets connect event bridge rule to the lambda function 200 | self.rule.add_target(targets.LambdaFunction(self.lambda_function)) 201 | -------------------------------------------------------------------------------- /app/ai_generator/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import random 5 | import re 6 | import uuid 7 | from datetime import datetime, timedelta 8 | from decimal import Decimal, InvalidOperation 9 | from io import BytesIO 10 | 11 | import boto3 12 | from botocore.exceptions import ClientError 13 | from fpdf import FPDF 14 | 15 | # Configure logging 16 | logger = logging.getLogger() 17 | logger.setLevel(logging.INFO) 18 | 19 | 20 | def convert_to_json_serializable(data): 21 | """Recursively convert Decimal and other non-serializable types to JSON-compatible types.""" 22 | if isinstance(data, dict): 23 | return {key: convert_to_json_serializable(value) for key, value in data.items()} 24 | elif isinstance(data, list): 25 | return [convert_to_json_serializable(item) for item in data] 26 | elif isinstance(data, Decimal): 27 | return float(data) # Convert Decimal to float for JSON serialization 28 | elif isinstance(data, datetime): 29 | return data.isoformat() # Convert datetime to an ISO 8601 formatted string 30 | else: 31 | return data # Return as is 32 | 33 | 34 | def convert_to_string(data): 35 | """Recursively convert float values in a dictionary to string.""" 36 | if isinstance(data, dict): 37 | return {key: convert_to_string(value) for key, value in data.items()} 38 | elif isinstance(data, list): 39 | return [convert_to_string(item) for item in data] 40 | elif isinstance(data, float): 41 | try: 42 | return str(data) # Convert float to string 43 | except InvalidOperation: 44 | return "0" # Fallback to zero or another default value as a string 45 | else: 46 | return data # Return as is 47 | 48 | 49 | def get_guardduty_finding(client: boto3.client, detector_id: str, finding_id: str): 50 | """Retrieve details of a GuardDuty finding.""" 51 | try: 52 | finding = client.get_findings(DetectorId=detector_id, FindingIds=[finding_id])["Findings"][ 53 | 0 54 | ] 55 | logger.info("Successfully retrieved GuardDuty finding details") 56 | return finding 57 | except Exception as e: 58 | logger.error("Error retrieving GuardDuty finding: %s", e) 59 | raise 60 | 61 | 62 | def get_graph_arn(client: boto3.client): 63 | """Retrieve the graph ARN for Amazon Detective.""" 64 | try: 65 | graphs = client.list_graphs() 66 | if not graphs.get("GraphList", []): 67 | raise Exception("No graphs found in Amazon Detective.") 68 | graph_arn = graphs["GraphList"][0]["Arn"] 69 | logger.info("Retrieved Amazon Detective graph ARN: %s", graph_arn) 70 | return graph_arn 71 | except Exception as e: 72 | logger.error("Error retrieving Amazon Detective graph ARN: %s", e) 73 | raise 74 | 75 | 76 | def get_entity_details(client: boto3.client, graph_arn: str, account_id: str): 77 | """Retrieve entity details from Amazon Detective.""" 78 | try: 79 | entity_data = client.get_members(GraphArn=graph_arn, AccountIds=[account_id]) 80 | entity_details = entity_data["MemberDetails"][0] 81 | logger.info("Successfully retrieved entity details from Amazon Detective") 82 | return entity_details 83 | except Exception as e: 84 | logger.error("Error retrieving entity details from Detective: %s", e) 85 | raise 86 | 87 | 88 | def list_datasource_packages(client: boto3.client, graph_arn): 89 | """List data source packages in Detective.""" 90 | try: 91 | response = client.list_datasource_packages(GraphArn=graph_arn) 92 | logger.info("Successfully retrieved datasource packages from Amazon Detective") 93 | return response["DatasourcePackages"] 94 | except Exception as e: 95 | logger.error(f"Error retrieving data source packages: {e}") 96 | return None 97 | 98 | 99 | def get_all_detective_entities(client: boto3.client, graph_arn: str) -> list: 100 | """ 101 | Retrieve all entity details from Amazon Detective. 102 | 103 | Args: 104 | client (boto3.client): The boto3 Detective client. 105 | graph_arn (str): The ARN of the Detective graph. 106 | 107 | Returns: 108 | list: A list of member details. 109 | 110 | Raises: 111 | ClientError: If there's an issue with the AWS API call. 112 | Exception: For any other unexpected errors. 113 | """ 114 | try: 115 | # Call list_members directly since it's not paginated 116 | response = client.list_members(GraphArn=graph_arn) 117 | all_members = response.get("MemberDetails", []) 118 | 119 | logger.info( 120 | f"Successfully retrieved {len(all_members)} entity details from Amazon Detective" 121 | ) 122 | return all_members 123 | 124 | except ClientError as e: 125 | error_code = e.response["Error"]["Code"] 126 | error_message = e.response["Error"]["Message"] 127 | logger.error( 128 | f"AWS API error when retrieving entities from Detective: {error_code} - {error_message}" 129 | ) 130 | raise 131 | 132 | except Exception as e: 133 | logger.error(f"Unexpected error retrieving entities from Detective: {str(e)}") 134 | raise 135 | 136 | 137 | def invoke_bedrock_model( 138 | client: boto3.client, finding: dict, detective_entities: dict, event: dict, context: dict 139 | ): 140 | """Invoke the Bedrock model and retrieve AI insights.""" 141 | try: 142 | messages = [ 143 | { 144 | "role": "user", 145 | "content": ( 146 | "You are a cybersecurity assistant that specializes in providing actionable" 147 | " solutions for security threats detected by AWS services. Here is the" 148 | " GuardDuty finding and relevant enrichment data from Amazon Detective:" 149 | f" {json.dumps(finding, indent=4)}. " 150 | f"Detective entities involved:" 151 | f" {json.dumps(detective_entities, indent=4)}. " 152 | "Based on this information,determine if this was a successful breach or a blocked attempt. Also," 153 | " provide information on the entities involved and if a security group is" 154 | " impacted and needs intervention.Also, provide specific actions I should take" 155 | " to remediate this issue and prevent future occurrences. " 156 | "The actions should come with a section header containing \"Remediation Actions:\"" 157 | ), 158 | } 159 | ] 160 | model_id = os.environ.get('BEDROCK_MODEL_ID', 'anthropic.claude-3-5-sonnet-20240620-v1:0') 161 | # Construct body arguments 162 | body_args = { 163 | "messages": messages, 164 | "max_tokens": 5000, 165 | "temperature": 0.7, 166 | } 167 | 168 | # Add additional body arguments from environment variables 169 | for key, value in os.environ.items(): 170 | if key.startswith('body_args_'): 171 | arg_name = key[10:] # Remove 'body_args_' prefix 172 | # Try to parse the value as JSON, if it fails, use the string value 173 | try: 174 | body_args[arg_name] = json.loads(value) 175 | except json.JSONDecodeError: 176 | body_args[arg_name] = value 177 | logger.info(f"Body args:{json.dumps(body_args)}") 178 | 179 | ai_response = client.invoke_model( 180 | modelId=model_id, 181 | accept="application/json", 182 | body=json.dumps(body_args) 183 | ) 184 | 185 | ai_insights = json.loads(ai_response["body"].read().decode("utf-8")) 186 | logger.info(f"AI Response: {json.dumps(ai_insights)}") 187 | return ai_insights.get("content", [{}])[0].get("text", "") 188 | except ClientError as e: 189 | if e.response["Error"]["Code"] == "ThrottlingException": 190 | # Handle throttling by scheduling a retry event 191 | return schedule_retry(event, context) 192 | else: 193 | logger.error("Error performing AI analysis with Bedrock: %s", e) 194 | raise 195 | except Exception as e: 196 | logger.error("Unexpected error: %s", e) 197 | raise 198 | 199 | 200 | def schedule_retry(event: dict, context: dict) -> str: 201 | client = boto3.client("events") 202 | """Schedule a one-time retry event with a random delay (up to 10 minutes).""" 203 | delay_seconds = random.randint(1, 600) # Random delay between 1 and 600 seconds 204 | future_time = datetime.utcnow() + timedelta(seconds=delay_seconds) 205 | event_id = str(uuid.uuid4()) 206 | 207 | # Extract relevant details from the original event 208 | try: 209 | account_id = event["account"] 210 | region = event["region"] 211 | finding_id = event["detail"]["id"] 212 | detector_id = event["detail"]["service"]["detectorId"] 213 | except KeyError as e: 214 | logger.error(f"Missing expected key in event: {e}") 215 | raise ValueError("Invalid event structure") 216 | 217 | logger.info(f"Scheduling a retry in {delay_seconds} seconds.") 218 | 219 | # Format future time into a cron expression 220 | cron_expression = ( 221 | f"cron({future_time.minute} {future_time.hour} {future_time.day} {future_time.month} ?" 222 | f" {future_time.year})" 223 | ) 224 | 225 | logger.info( 226 | f"Attempting to schedule rule: Retry-{event_id} with ScheduleExpression: {cron_expression}" 227 | ) 228 | 229 | try: 230 | # Create a one-time EventBridge rule to invoke the Lambda function 231 | rule_name = f"RetryLambdaInvocation-{event_id}" 232 | client.put_rule(Name=rule_name, ScheduleExpression=cron_expression, State="ENABLED") 233 | logger.info(f"Rule {rule_name} created successfully.") 234 | 235 | # Add a target for the EventBridge rule to invoke this Lambda function 236 | lambda_arn = f"arn:aws:lambda:{region}:{account_id}:function:{context.function_name}" 237 | client.put_targets( 238 | Rule=rule_name, 239 | Targets=[ 240 | { 241 | "Id": event_id, 242 | "Arn": lambda_arn, 243 | "Input": json.dumps( 244 | { 245 | "version": event["version"], 246 | "id": event["id"], 247 | "detail-type": event["detail-type"], 248 | "source": event["source"], 249 | "account": account_id, 250 | "time": event["time"], 251 | "region": region, 252 | "resources": event["resources"], 253 | "detail": { 254 | "schemaVersion": event["detail"]["schemaVersion"], 255 | "accountId": account_id, 256 | "region": region, 257 | "partition": event["detail"]["partition"], 258 | "id": finding_id, 259 | "service": {"detectorId": detector_id}, 260 | "retryRuleName": f"RetryLambdaInvocation-{event_id}", 261 | }, 262 | } 263 | ), 264 | } 265 | ], 266 | ) 267 | logger.info(f"Retry scheduled with a {delay_seconds}-second delay.") 268 | return "rescheduled" 269 | 270 | except ClientError as e: 271 | logger.error("Failed to schedule retry: %s", e) 272 | except Exception as e: 273 | logger.error("An unexpected error occurred: %s", e) 274 | 275 | 276 | def generate_pdf(ai_insights, guardduty_finding) -> BytesIO: 277 | """Generate a PDF file with AI insights and GuardDuty finding details, including formatting improvements. 278 | """ 279 | 280 | # Create a PDF using FPDF 281 | pdf = FPDF() 282 | pdf.set_auto_page_break(auto=True, margin=15) 283 | pdf.add_page() 284 | 285 | # Add Title 286 | pdf.set_font("Arial", "B", 16) 287 | pdf.cell(200, 10, txt="GuardDuty Finding Report", ln=True, align="C") 288 | pdf.cell(200, 10, txt=guardduty_finding.get("Type", "N/A"), ln=True, align="C") 289 | 290 | # Metadata (generation time) 291 | pdf.set_font("Arial", "", 12) 292 | pdf.cell(200, 10, txt=f"Generated on: {datetime.utcnow().isoformat()}", ln=True, align="C") 293 | 294 | # Add Section for GuardDuty Finding Details 295 | pdf.ln(10) 296 | pdf.set_font("Arial", "B", 14) 297 | pdf.cell(200, 10, txt="GuardDuty Finding Details", ln=True) 298 | 299 | # GuardDuty Finding Information 300 | pdf.set_font("Arial", "", 12) 301 | finding_details = ( 302 | f"Finding Type: {guardduty_finding.get('Type', 'N/A')}\n" 303 | f"Finding ID: {guardduty_finding.get('Id', 'N/A')}\n" 304 | f"Severity: {guardduty_finding.get('Severity', 'N/A')}\n" 305 | f"Account ID: {guardduty_finding.get('AccountId', 'N/A')}\n" 306 | f"Region: {guardduty_finding.get('Region', 'N/A')}\n" 307 | ) 308 | pdf.multi_cell(0, 10, txt=finding_details) 309 | 310 | # Construct GuardDuty and Detective Links 311 | region = guardduty_finding.get("Region", "us-east-1") 312 | finding_id = guardduty_finding.get("Id", "unknown") 313 | 314 | guardduty_link = ( 315 | f"https://{region}.console.aws.amazon.com/guardduty/home?region={region}#/findings?" 316 | f"search=id%3D{finding_id}¯os=current" 317 | ) 318 | detective_link = ( 319 | f"https://{region}.console.aws.amazon.com/detective/home?region={region}#search?" 320 | f"searchType=Finding&searchText={finding_id}" 321 | ) 322 | 323 | # Add clickable GuardDuty and Detective links 324 | pdf.ln(10) 325 | pdf.set_font("Arial", "B", 12) 326 | pdf.cell(200, 10, txt="Relevant Links", ln=True) 327 | 328 | pdf.set_font("Arial", "", 12) 329 | pdf.set_text_color(0, 0, 255) # Blue for links 330 | pdf.cell(200, 10, txt="View Finding in GuardDuty Console", ln=True, link=guardduty_link) 331 | pdf.cell(200, 10, txt="View Finding in Detective Console", ln=True, link=detective_link) 332 | 333 | # Reset text color 334 | pdf.set_text_color(0, 0, 0) 335 | 336 | # Add a Section for AI Insights with Proper Formatting and Line Breaks 337 | pdf.ln(10) 338 | pdf.set_font("Arial", "B", 14) 339 | pdf.cell(200, 10, txt="AI Insights", ln=True) 340 | 341 | # Parse AI insights line by line, handling double newlines 342 | insights_lines = re.split(r"\n\s*\n", ai_insights.strip()) # Split by double newlines 343 | 344 | for section in insights_lines: 345 | for line in section.split("\n"): 346 | # Check for specific headers 347 | if line.strip() in ["Analysis:", "Remediation Actions:", "Recommended Actions:"]: 348 | pdf.set_font("Arial", "B", 12) # Set font for H3 349 | pdf.cell(0, 10, txt=line.strip(), ln=True) # Add header 350 | elif line.strip() in [ 351 | "Entities Involved:", 352 | "Security Group Impact:", 353 | "Attempt Status:", 354 | ]: 355 | pdf.set_font("Arial", "B", 10) # Set font for H3 356 | pdf.cell(0, 10, txt=line.strip(), ln=True) # Add header 357 | else: 358 | pdf.set_font("Arial", "", 10) # Set back to normal font 359 | pdf.multi_cell(0, 10, txt=line.strip()) # Add regular text 360 | 361 | pdf.ln(5) # Add some space between sections 362 | 363 | # Add Summary or Conclusion Section 364 | pdf.ln(10) 365 | pdf.set_font("Arial", "B", 14) 366 | pdf.cell(200, 10, txt="Conclusion and Recommended Actions", ln=True) 367 | 368 | summary_text = ( 369 | "The above insights and recommendations provide detailed information on mitigating the" 370 | " identified threat. Please ensure the recommended security actions are promptly applied to" 371 | " minimize future risks." 372 | ) 373 | pdf.set_font("Arial", "", 12) 374 | pdf.multi_cell(0, 10, txt=summary_text) 375 | 376 | # Generate PDF in memory 377 | pdf_buffer = BytesIO() 378 | pdf_output = pdf.output(dest="S").encode("latin1") # Return as string and encode 379 | pdf_buffer.write(pdf_output) 380 | pdf_buffer.seek(0) 381 | 382 | return pdf_buffer 383 | 384 | 385 | def upload_pdf_to_s3(client: boto3.client, pdf_buffer: BytesIO, file_name: str, bucket_name: str): 386 | """Upload the PDF to S3 and return the pre-signed URL.""" 387 | try: 388 | # Upload the PDF file to S3 389 | client.put_object( 390 | Bucket=bucket_name, Key=file_name, Body=pdf_buffer, ContentType="application/pdf" 391 | ) 392 | 393 | # Generate a pre-signed URL for the PDF 394 | pre_signed_url = client.generate_presigned_url( 395 | "get_object", 396 | Params={"Bucket": bucket_name, "Key": file_name}, 397 | ExpiresIn=3600, # URL expires in 1 hour 398 | ) 399 | 400 | return pre_signed_url 401 | except Exception as e: 402 | logger.error(f"Error uploading PDF to S3: {e}") 403 | raise 404 | 405 | 406 | def send_sns_notification(client: boto3.client, pre_signed_url: str, topic_arn: str): 407 | """Send SNS notification with the pre-signed URL.""" 408 | try: 409 | # Ensure URL is stripped and encoded 410 | pre_signed_url = pre_signed_url.strip().replace(" ", "%20") 411 | logger.info( 412 | f"Encoded Pre-signed URL: {pre_signed_url}" 413 | ) # Log the encoded URL for debugging 414 | 415 | # Compose message in a simplified HTML format 416 | message = ( 417 | "New GuardDuty finding. Enriched PDF with AI remediation's can be downloaded here: " 418 | f"{pre_signed_url}" 419 | ) 420 | 421 | # Create the SNS message structure with JSON to support HTML 422 | message_structure = { 423 | "default": "New GuardDuty finding with enriched PDF. Click the link to download.", 424 | "email": message, 425 | } 426 | 427 | # Publish to SNS 428 | client.publish( 429 | TopicArn=topic_arn, 430 | Message=json.dumps(message_structure), 431 | Subject="GuardDuty Finding Enrichment Report", 432 | MessageStructure="json", 433 | ) 434 | 435 | logger.info("Notification sent to SNS") 436 | except Exception as e: 437 | logger.error(f"Error sending SNS notification: {e}") 438 | raise 439 | 440 | 441 | def remove_retry_event_rule(client: boto3.client, event_rule_name: str): 442 | try: 443 | logger.info(f"Deleting EventBridge rule: {event_rule_name}") 444 | client.delete_rule(Name=event_rule_name) 445 | logger.info(f"Deleted EventBridge rule: {event_rule_name}") 446 | except Exception as e: 447 | logger.error(f"Error deleting EventBridge rule: {e}") 448 | 449 | 450 | def lambda_handler(event, context): 451 | logger.info("Received event: %s", json.dumps(event, indent=4)) 452 | sns_topic_arn = os.getenv("AI_REPORTS_TOPIC_ARN") 453 | s3_bucket_name = os.getenv("AI_REPORTS_BUCKET_NAME") 454 | detective_client = boto3.client("detective") 455 | guardduty_client = boto3.client("guardduty") 456 | bedrock_client = boto3.client("bedrock-runtime") 457 | s3_client = boto3.client("s3") 458 | sns_client = boto3.client("sns") 459 | if not s3_bucket_name: 460 | logger.error("Environment variable AI_REPORTS_BUCKET_NAME is not set") 461 | raise ValueError("Environment variable AI_REPORTS_BUCKET_NAME is not set") 462 | if not sns_topic_arn: 463 | logger.error("Environment variable AI_REPORTS_TOPIC_ARN is not set") 464 | raise ValueError("Environment variable AI_REPORTS_TOPIC_ARN is not set") 465 | try: 466 | finding_id = event["detail"]["id"] 467 | detector_id = event["detail"]["service"]["detectorId"] 468 | logger.info( 469 | f"Processing GuardDuty finding: {finding_id} for detector with ID: {detector_id}" 470 | ) 471 | # Get GuardDuty finding details 472 | finding = get_guardduty_finding(guardduty_client, detector_id, finding_id) 473 | # only proceed if severity if bigger then 4.0 474 | if finding["Severity"] < 4.0: 475 | logger.info( 476 | f"Finding {finding_id} has severity of {finding['Severity']} not processing" 477 | ) 478 | return { 479 | "statusCode": 200, 480 | "body": json.dumps( 481 | f"Finding {finding_id} has severity of {finding['Severity']} not processing" 482 | ), 483 | } 484 | 485 | # Get the graph ARN for Amazon Detective 486 | graph_arn = get_graph_arn(detective_client) 487 | 488 | # Get all entity details from Amazon Detective 489 | detective_entities = get_all_detective_entities(detective_client, graph_arn) 490 | 491 | # Convert enriched_data to use Decimal for float types 492 | finding = convert_to_json_serializable(finding) 493 | detective_entities = convert_to_json_serializable(detective_entities) 494 | 495 | # AI Analysis with Amazon Bedrock 496 | ai_insights = invoke_bedrock_model( 497 | bedrock_client, finding, detective_entities, event, context 498 | ) 499 | if ai_insights == "rescheduled": 500 | return {"statusCode": 200, "body": json.dumps("Event scheduled.")} 501 | # Generate PDF with AI insights and GuardDuty information 502 | pdf_buffer = generate_pdf(ai_insights, finding) 503 | 504 | # Upload PDF to S3 and get pre-signed URL 505 | file_name = f"{finding_id}.pdf" 506 | 507 | pre_signed_url = upload_pdf_to_s3(s3_client, pdf_buffer, file_name, s3_bucket_name) 508 | 509 | # Send SNS notification with pre-signed URL 510 | send_sns_notification(sns_client, pre_signed_url, sns_topic_arn) 511 | 512 | # if case this is a rescheduled event, lets clean it to keep the event bridge clean 513 | # Check if 'retryRuleName' exists and remove the retry event rule if it does 514 | retry_rule_name = event["detail"].get("retryRuleName") # Use get() to avoid KeyError 515 | if retry_rule_name: 516 | remove_retry_event_rule(sns_client, retry_rule_name) 517 | except Exception as e: 518 | logger.error("Error in processing: %s", e) 519 | return {"statusCode": 500, "body": json.dumps(f"Error in processing: {e}")} 520 | return {"statusCode": 200, "body": json.dumps(ai_insights)} 521 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "assertpy" 5 | version = "1.1" 6 | description = "Simple assertion library for unit testing in python with a fluent API" 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "assertpy-1.1.tar.gz", hash = "sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833"}, 11 | ] 12 | 13 | [[package]] 14 | name = "astroid" 15 | version = "3.3.8" 16 | description = "An abstract syntax tree for Python with inference support." 17 | optional = false 18 | python-versions = ">=3.9.0" 19 | files = [ 20 | {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, 21 | {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, 22 | ] 23 | 24 | [package.dependencies] 25 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 26 | 27 | [[package]] 28 | name = "attrs" 29 | version = "24.3.0" 30 | description = "Classes Without Boilerplate" 31 | optional = false 32 | python-versions = ">=3.8" 33 | files = [ 34 | {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, 35 | {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, 36 | ] 37 | 38 | [package.extras] 39 | benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 40 | cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 41 | dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 42 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 43 | tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 44 | tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] 45 | 46 | [[package]] 47 | name = "aws-cdk-asset-awscli-v1" 48 | version = "2.2.217" 49 | description = "A library that contains the AWS CLI for use in Lambda Layers" 50 | optional = false 51 | python-versions = "~=3.8" 52 | files = [ 53 | {file = "aws_cdk.asset_awscli_v1-2.2.217-py3-none-any.whl", hash = "sha256:06a3f2bf2eb1a0067829743c2f514094dc21a958b4ad0ab58e589cc3763c6c77"}, 54 | {file = "aws_cdk_asset_awscli_v1-2.2.217.tar.gz", hash = "sha256:b827df8b01c90f165ddd807df833025936b41716322fbc5f76266514f54fb71a"}, 55 | ] 56 | 57 | [package.dependencies] 58 | jsii = ">=1.106.0,<2.0.0" 59 | publication = ">=0.0.3" 60 | typeguard = ">=2.13.3,<4.3.0" 61 | 62 | [[package]] 63 | name = "aws-cdk-asset-kubectl-v20" 64 | version = "2.1.3" 65 | description = "A Lambda Layer that contains kubectl v1.20" 66 | optional = false 67 | python-versions = "~=3.8" 68 | files = [ 69 | {file = "aws_cdk.asset_kubectl_v20-2.1.3-py3-none-any.whl", hash = "sha256:d5612e5bd03c215a28ce53193b1144ecf4e93b3b6779563c046a8a74d83a3979"}, 70 | {file = "aws_cdk_asset_kubectl_v20-2.1.3.tar.gz", hash = "sha256:237cd8530d9e8be0bbc7159af927dbb6b7f91bf3f4099c8ef4d9a213b34264be"}, 71 | ] 72 | 73 | [package.dependencies] 74 | jsii = ">=1.103.1,<2.0.0" 75 | publication = ">=0.0.3" 76 | typeguard = ">=2.13.3,<5.0.0" 77 | 78 | [[package]] 79 | name = "aws-cdk-asset-node-proxy-agent-v6" 80 | version = "2.1.0" 81 | description = "@aws-cdk/asset-node-proxy-agent-v6" 82 | optional = false 83 | python-versions = "~=3.8" 84 | files = [ 85 | {file = "aws_cdk.asset_node_proxy_agent_v6-2.1.0-py3-none-any.whl", hash = "sha256:24a388b69a44d03bae6dbf864c4e25ba650d4b61c008b4568b94ffbb9a69e40e"}, 86 | {file = "aws_cdk_asset_node_proxy_agent_v6-2.1.0.tar.gz", hash = "sha256:1f292c0631f86708ba4ee328b3a2b229f7e46ea1c79fbde567ee9eb119c2b0e2"}, 87 | ] 88 | 89 | [package.dependencies] 90 | jsii = ">=1.103.1,<2.0.0" 91 | publication = ">=0.0.3" 92 | typeguard = ">=2.13.3,<5.0.0" 93 | 94 | [[package]] 95 | name = "aws-cdk-lib" 96 | version = "2.130.0" 97 | description = "Version 2 of the AWS Cloud Development Kit library" 98 | optional = false 99 | python-versions = "~=3.8" 100 | files = [ 101 | {file = "aws-cdk-lib-2.130.0.tar.gz", hash = "sha256:b9ed68a5fd7f5b9056da58bd122c9c3faa6af1e92f4b6aff181a2ee57625aad1"}, 102 | {file = "aws_cdk_lib-2.130.0-py3-none-any.whl", hash = "sha256:03a98770dd58caa002ded8d2dcdd3f6f7451a95f86c8dba3b5f2b70e659429b3"}, 103 | ] 104 | 105 | [package.dependencies] 106 | "aws-cdk.asset-awscli-v1" = ">=2.2.202,<3.0.0" 107 | "aws-cdk.asset-kubectl-v20" = ">=2.1.2,<3.0.0" 108 | "aws-cdk.asset-node-proxy-agent-v6" = ">=2.0.1,<3.0.0" 109 | constructs = ">=10.0.0,<11.0.0" 110 | jsii = ">=1.94.0,<2.0.0" 111 | publication = ">=0.0.3" 112 | typeguard = ">=2.13.3,<2.14.0" 113 | 114 | [[package]] 115 | name = "black" 116 | version = "24.10.0" 117 | description = "The uncompromising code formatter." 118 | optional = false 119 | python-versions = ">=3.9" 120 | files = [ 121 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 122 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 123 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 124 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 125 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 126 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 127 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 128 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 129 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 130 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 131 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 132 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 133 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 134 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 135 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 136 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 137 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 138 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 139 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 140 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 141 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 142 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 143 | ] 144 | 145 | [package.dependencies] 146 | click = ">=8.0.0" 147 | mypy-extensions = ">=0.4.3" 148 | packaging = ">=22.0" 149 | pathspec = ">=0.9.0" 150 | platformdirs = ">=2" 151 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 152 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 153 | 154 | [package.extras] 155 | colorama = ["colorama (>=0.4.3)"] 156 | d = ["aiohttp (>=3.10)"] 157 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 158 | uvloop = ["uvloop (>=0.15.2)"] 159 | 160 | [[package]] 161 | name = "boolean-py" 162 | version = "4.0" 163 | description = "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL." 164 | optional = false 165 | python-versions = "*" 166 | files = [ 167 | {file = "boolean.py-4.0-py3-none-any.whl", hash = "sha256:2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd"}, 168 | {file = "boolean.py-4.0.tar.gz", hash = "sha256:17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4"}, 169 | ] 170 | 171 | [[package]] 172 | name = "boto3" 173 | version = "1.34.51" 174 | description = "The AWS SDK for Python" 175 | optional = false 176 | python-versions = ">= 3.8" 177 | files = [ 178 | {file = "boto3-1.34.51-py3-none-any.whl", hash = "sha256:67732634dc7d0afda879bd9a5e2d0818a2c14a98bef766b95a3e253ea5104cb9"}, 179 | {file = "boto3-1.34.51.tar.gz", hash = "sha256:2cd9463e738a184cbce8a6824027c22163c5f73e277a35ff5aa0fb0e845b4301"}, 180 | ] 181 | 182 | [package.dependencies] 183 | botocore = ">=1.34.51,<1.35.0" 184 | jmespath = ">=0.7.1,<2.0.0" 185 | s3transfer = ">=0.10.0,<0.11.0" 186 | 187 | [package.extras] 188 | crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] 189 | 190 | [[package]] 191 | name = "botocore" 192 | version = "1.34.162" 193 | description = "Low-level, data-driven core of boto 3." 194 | optional = false 195 | python-versions = ">=3.8" 196 | files = [ 197 | {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, 198 | {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, 199 | ] 200 | 201 | [package.dependencies] 202 | jmespath = ">=0.7.1,<2.0.0" 203 | python-dateutil = ">=2.1,<3.0.0" 204 | urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} 205 | 206 | [package.extras] 207 | crt = ["awscrt (==0.21.2)"] 208 | 209 | [[package]] 210 | name = "cachecontrol" 211 | version = "0.14.1" 212 | description = "httplib2 caching for requests" 213 | optional = false 214 | python-versions = ">=3.8" 215 | files = [ 216 | {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, 217 | {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, 218 | ] 219 | 220 | [package.dependencies] 221 | filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} 222 | msgpack = ">=0.5.2,<2.0.0" 223 | requests = ">=2.16.0" 224 | 225 | [package.extras] 226 | dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] 227 | filecache = ["filelock (>=3.8.0)"] 228 | redis = ["redis (>=2.10.5)"] 229 | 230 | [[package]] 231 | name = "cachetools" 232 | version = "5.5.0" 233 | description = "Extensible memoizing collections and decorators" 234 | optional = false 235 | python-versions = ">=3.7" 236 | files = [ 237 | {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, 238 | {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, 239 | ] 240 | 241 | [[package]] 242 | name = "cattrs" 243 | version = "24.1.2" 244 | description = "Composable complex class support for attrs and dataclasses." 245 | optional = false 246 | python-versions = ">=3.8" 247 | files = [ 248 | {file = "cattrs-24.1.2-py3-none-any.whl", hash = "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0"}, 249 | {file = "cattrs-24.1.2.tar.gz", hash = "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85"}, 250 | ] 251 | 252 | [package.dependencies] 253 | attrs = ">=23.1.0" 254 | exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} 255 | typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} 256 | 257 | [package.extras] 258 | bson = ["pymongo (>=4.4.0)"] 259 | cbor2 = ["cbor2 (>=5.4.6)"] 260 | msgpack = ["msgpack (>=1.0.5)"] 261 | msgspec = ["msgspec (>=0.18.5)"] 262 | orjson = ["orjson (>=3.9.2)"] 263 | pyyaml = ["pyyaml (>=6.0)"] 264 | tomlkit = ["tomlkit (>=0.11.8)"] 265 | ujson = ["ujson (>=5.7.0)"] 266 | 267 | [[package]] 268 | name = "cdk-nag" 269 | version = "2.28.48" 270 | description = "Check CDK v2 applications for best practices using a combination on available rule packs." 271 | optional = false 272 | python-versions = "~=3.8" 273 | files = [ 274 | {file = "cdk-nag-2.28.48.tar.gz", hash = "sha256:602d8a91252424f557f2dc991dca413dbdd7ae656303d961a849634a4181532a"}, 275 | {file = "cdk_nag-2.28.48-py3-none-any.whl", hash = "sha256:8f62603886eac9072aa77fc79700efdc6d1ac44a7b8537516f8adf849d59dae9"}, 276 | ] 277 | 278 | [package.dependencies] 279 | aws-cdk-lib = ">=2.116.0,<3.0.0" 280 | constructs = ">=10.0.5,<11.0.0" 281 | jsii = ">=1.94.0,<2.0.0" 282 | publication = ">=0.0.3" 283 | typeguard = ">=2.13.3,<2.14.0" 284 | 285 | [[package]] 286 | name = "certifi" 287 | version = "2024.12.14" 288 | description = "Python package for providing Mozilla's CA Bundle." 289 | optional = false 290 | python-versions = ">=3.6" 291 | files = [ 292 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 293 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 294 | ] 295 | 296 | [[package]] 297 | name = "cffi" 298 | version = "1.17.1" 299 | description = "Foreign Function Interface for Python calling C code." 300 | optional = false 301 | python-versions = ">=3.8" 302 | files = [ 303 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 304 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 305 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 306 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 307 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 308 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 309 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 310 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 311 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 312 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 313 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 314 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 315 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 316 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 317 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 318 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 319 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 320 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 321 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 322 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 323 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 324 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 325 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 326 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 327 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 328 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 329 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 330 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 331 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 332 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 333 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 334 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 335 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 336 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 337 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 338 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 339 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 340 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 341 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 342 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 343 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 344 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 345 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 346 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 347 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 348 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 349 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 350 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 351 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 352 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 353 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 354 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 355 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 356 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 357 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 358 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 359 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 360 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 361 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 362 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 363 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 364 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 365 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 366 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 367 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 368 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 369 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 370 | ] 371 | 372 | [package.dependencies] 373 | pycparser = "*" 374 | 375 | [[package]] 376 | name = "cfgv" 377 | version = "3.4.0" 378 | description = "Validate configuration and produce human readable error messages." 379 | optional = false 380 | python-versions = ">=3.8" 381 | files = [ 382 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 383 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 384 | ] 385 | 386 | [[package]] 387 | name = "chardet" 388 | version = "5.2.0" 389 | description = "Universal encoding detector for Python 3" 390 | optional = false 391 | python-versions = ">=3.7" 392 | files = [ 393 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 394 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 395 | ] 396 | 397 | [[package]] 398 | name = "charset-normalizer" 399 | version = "3.4.1" 400 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 401 | optional = false 402 | python-versions = ">=3.7" 403 | files = [ 404 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 405 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 406 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 407 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 408 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 409 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 410 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 411 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 412 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 413 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 414 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 415 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 416 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 417 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 418 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 419 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 420 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 421 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 422 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 423 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 424 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 425 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 426 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 427 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 428 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 429 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 430 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 431 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 432 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 433 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 434 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 435 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 436 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 437 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 438 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 439 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 440 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 441 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 442 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 443 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 444 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 445 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 446 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 447 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 448 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 449 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 450 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 451 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 452 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 453 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 454 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 455 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 456 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 457 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 458 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 459 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 460 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 461 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 462 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 463 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 464 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 465 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 466 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 467 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 468 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 469 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 470 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 471 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 472 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 473 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 474 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 475 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 476 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 477 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 478 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 479 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 480 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 481 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 482 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 483 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 484 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 485 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 486 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 487 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 488 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 489 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 490 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 491 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 492 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 493 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 494 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 495 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 496 | ] 497 | 498 | [[package]] 499 | name = "click" 500 | version = "8.1.8" 501 | description = "Composable command line interface toolkit" 502 | optional = false 503 | python-versions = ">=3.7" 504 | files = [ 505 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 506 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 507 | ] 508 | 509 | [package.dependencies] 510 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 511 | 512 | [[package]] 513 | name = "colorama" 514 | version = "0.4.6" 515 | description = "Cross-platform colored terminal text." 516 | optional = false 517 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 518 | files = [ 519 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 520 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 521 | ] 522 | 523 | [[package]] 524 | name = "constructs" 525 | version = "10.3.0" 526 | description = "A programming model for software-defined state" 527 | optional = false 528 | python-versions = "~=3.7" 529 | files = [ 530 | {file = "constructs-10.3.0-py3-none-any.whl", hash = "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2"}, 531 | {file = "constructs-10.3.0.tar.gz", hash = "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"}, 532 | ] 533 | 534 | [package.dependencies] 535 | jsii = ">=1.90.0,<2.0.0" 536 | publication = ">=0.0.3" 537 | typeguard = ">=2.13.3,<2.14.0" 538 | 539 | [[package]] 540 | name = "coverage" 541 | version = "7.4.3" 542 | description = "Code coverage measurement for Python" 543 | optional = false 544 | python-versions = ">=3.8" 545 | files = [ 546 | {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, 547 | {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, 548 | {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, 549 | {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, 550 | {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, 551 | {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, 552 | {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, 553 | {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, 554 | {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, 555 | {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, 556 | {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, 557 | {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, 558 | {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, 559 | {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, 560 | {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, 561 | {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, 562 | {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, 563 | {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, 564 | {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, 565 | {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, 566 | {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, 567 | {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, 568 | {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, 569 | {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, 570 | {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, 571 | {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, 572 | {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, 573 | {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, 574 | {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, 575 | {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, 576 | {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, 577 | {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, 578 | {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, 579 | {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, 580 | {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, 581 | {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, 582 | {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, 583 | {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, 584 | {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, 585 | {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, 586 | {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, 587 | {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, 588 | {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, 589 | {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, 590 | {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, 591 | {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, 592 | {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, 593 | {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, 594 | {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, 595 | {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, 596 | {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, 597 | {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, 598 | ] 599 | 600 | [package.dependencies] 601 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 602 | 603 | [package.extras] 604 | toml = ["tomli"] 605 | 606 | [[package]] 607 | name = "cyclonedx-python-lib" 608 | version = "6.4.4" 609 | description = "Python library for CycloneDX" 610 | optional = false 611 | python-versions = ">=3.8,<4.0" 612 | files = [ 613 | {file = "cyclonedx_python_lib-6.4.4-py3-none-any.whl", hash = "sha256:c366619cc4effd528675f1f7a7a00be30b6695ff03f49c64880ad15acbebc341"}, 614 | {file = "cyclonedx_python_lib-6.4.4.tar.gz", hash = "sha256:1b6f9109b6b9e91636dff822c2de90a05c0c8af120317713c1b879dbfdebdff8"}, 615 | ] 616 | 617 | [package.dependencies] 618 | license-expression = ">=30,<31" 619 | packageurl-python = ">=0.11,<2" 620 | py-serializable = ">=0.16,<2" 621 | sortedcontainers = ">=2.4.0,<3.0.0" 622 | 623 | [package.extras] 624 | json-validation = ["jsonschema[format] (>=4.18,<5.0)"] 625 | validation = ["jsonschema[format] (>=4.18,<5.0)", "lxml (>=4,<6)"] 626 | xml-validation = ["lxml (>=4,<6)"] 627 | 628 | [[package]] 629 | name = "defusedxml" 630 | version = "0.7.1" 631 | description = "XML bomb protection for Python stdlib modules" 632 | optional = false 633 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 634 | files = [ 635 | {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, 636 | {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, 637 | ] 638 | 639 | [[package]] 640 | name = "dill" 641 | version = "0.3.9" 642 | description = "serialize all of Python" 643 | optional = false 644 | python-versions = ">=3.8" 645 | files = [ 646 | {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, 647 | {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, 648 | ] 649 | 650 | [package.extras] 651 | graph = ["objgraph (>=1.7.2)"] 652 | profile = ["gprof2dot (>=2022.7.29)"] 653 | 654 | [[package]] 655 | name = "distlib" 656 | version = "0.3.9" 657 | description = "Distribution utilities" 658 | optional = false 659 | python-versions = "*" 660 | files = [ 661 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 662 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 663 | ] 664 | 665 | [[package]] 666 | name = "exceptiongroup" 667 | version = "1.2.2" 668 | description = "Backport of PEP 654 (exception groups)" 669 | optional = false 670 | python-versions = ">=3.7" 671 | files = [ 672 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 673 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 674 | ] 675 | 676 | [package.extras] 677 | test = ["pytest (>=6)"] 678 | 679 | [[package]] 680 | name = "filelock" 681 | version = "3.16.1" 682 | description = "A platform independent file lock." 683 | optional = false 684 | python-versions = ">=3.8" 685 | files = [ 686 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 687 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 688 | ] 689 | 690 | [package.extras] 691 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 692 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 693 | typing = ["typing-extensions (>=4.12.2)"] 694 | 695 | [[package]] 696 | name = "flake8" 697 | version = "7.1.1" 698 | description = "the modular source code checker: pep8 pyflakes and co" 699 | optional = false 700 | python-versions = ">=3.8.1" 701 | files = [ 702 | {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, 703 | {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, 704 | ] 705 | 706 | [package.dependencies] 707 | mccabe = ">=0.7.0,<0.8.0" 708 | pycodestyle = ">=2.12.0,<2.13.0" 709 | pyflakes = ">=3.2.0,<3.3.0" 710 | 711 | [[package]] 712 | name = "fpdf" 713 | version = "1.7.2" 714 | description = "Simple PDF generation for Python" 715 | optional = false 716 | python-versions = "*" 717 | files = [ 718 | {file = "fpdf-1.7.2.tar.gz", hash = "sha256:125840783289e7d12552b1e86ab692c37322e7a65b96a99e0ea86cca041b6779"}, 719 | ] 720 | 721 | [[package]] 722 | name = "html5lib" 723 | version = "1.1" 724 | description = "HTML parser based on the WHATWG HTML specification" 725 | optional = false 726 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 727 | files = [ 728 | {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, 729 | {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, 730 | ] 731 | 732 | [package.dependencies] 733 | six = ">=1.9" 734 | webencodings = "*" 735 | 736 | [package.extras] 737 | all = ["chardet (>=2.2)", "genshi", "lxml"] 738 | chardet = ["chardet (>=2.2)"] 739 | genshi = ["genshi"] 740 | lxml = ["lxml"] 741 | 742 | [[package]] 743 | name = "identify" 744 | version = "2.6.4" 745 | description = "File identification library for Python" 746 | optional = false 747 | python-versions = ">=3.9" 748 | files = [ 749 | {file = "identify-2.6.4-py2.py3-none-any.whl", hash = "sha256:993b0f01b97e0568c179bb9196391ff391bfb88a99099dbf5ce392b68f42d0af"}, 750 | {file = "identify-2.6.4.tar.gz", hash = "sha256:285a7d27e397652e8cafe537a6cc97dd470a970f48fb2e9d979aa38eae5513ac"}, 751 | ] 752 | 753 | [package.extras] 754 | license = ["ukkonen"] 755 | 756 | [[package]] 757 | name = "idna" 758 | version = "3.10" 759 | description = "Internationalized Domain Names in Applications (IDNA)" 760 | optional = false 761 | python-versions = ">=3.6" 762 | files = [ 763 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 764 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 765 | ] 766 | 767 | [package.extras] 768 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 769 | 770 | [[package]] 771 | name = "importlib-resources" 772 | version = "6.4.5" 773 | description = "Read resources from Python packages" 774 | optional = false 775 | python-versions = ">=3.8" 776 | files = [ 777 | {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, 778 | {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, 779 | ] 780 | 781 | [package.extras] 782 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 783 | cover = ["pytest-cov"] 784 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 785 | enabler = ["pytest-enabler (>=2.2)"] 786 | test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] 787 | type = ["pytest-mypy"] 788 | 789 | [[package]] 790 | name = "iniconfig" 791 | version = "2.0.0" 792 | description = "brain-dead simple config-ini parsing" 793 | optional = false 794 | python-versions = ">=3.7" 795 | files = [ 796 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 797 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 798 | ] 799 | 800 | [[package]] 801 | name = "isort" 802 | version = "5.13.2" 803 | description = "A Python utility / library to sort Python imports." 804 | optional = false 805 | python-versions = ">=3.8.0" 806 | files = [ 807 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 808 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 809 | ] 810 | 811 | [package.extras] 812 | colors = ["colorama (>=0.4.6)"] 813 | 814 | [[package]] 815 | name = "jmespath" 816 | version = "1.0.1" 817 | description = "JSON Matching Expressions" 818 | optional = false 819 | python-versions = ">=3.7" 820 | files = [ 821 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 822 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 823 | ] 824 | 825 | [[package]] 826 | name = "jsii" 827 | version = "1.106.0" 828 | description = "Python client for jsii runtime" 829 | optional = false 830 | python-versions = "~=3.8" 831 | files = [ 832 | {file = "jsii-1.106.0-py3-none-any.whl", hash = "sha256:5a44d7c3a5a326fa3d9befdb3770b380057e0a61e3804e7c4907f70d76afaaa2"}, 833 | {file = "jsii-1.106.0.tar.gz", hash = "sha256:c79c47899f53a7c3c4b20f80d3cd306628fe9ed1852eee970324c71eba1d974e"}, 834 | ] 835 | 836 | [package.dependencies] 837 | attrs = ">=21.2,<25.0" 838 | cattrs = ">=1.8,<24.2" 839 | importlib-resources = ">=5.2.0" 840 | publication = ">=0.0.3" 841 | python-dateutil = "*" 842 | typeguard = ">=2.13.3,<4.5.0" 843 | typing-extensions = ">=3.8,<5.0" 844 | 845 | [[package]] 846 | name = "license-expression" 847 | version = "30.4.0" 848 | description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." 849 | optional = false 850 | python-versions = ">=3.9" 851 | files = [ 852 | {file = "license_expression-30.4.0-py3-none-any.whl", hash = "sha256:7c8f240c6e20d759cb8455e49cb44a923d9e25c436bf48d7e5b8eea660782c04"}, 853 | {file = "license_expression-30.4.0.tar.gz", hash = "sha256:6464397f8ed4353cc778999caec43b099f8d8d5b335f282e26a9eb9435522f05"}, 854 | ] 855 | 856 | [package.dependencies] 857 | "boolean.py" = ">=4.0" 858 | 859 | [package.extras] 860 | docs = ["Sphinx (>=5.0.2)", "doc8 (>=0.11.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-reredirects (>=0.1.2)", "sphinx-rtd-dark-mode (>=1.3.0)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-apidoc (>=0.4.0)"] 861 | testing = ["black", "isort", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] 862 | 863 | [[package]] 864 | name = "markdown-it-py" 865 | version = "3.0.0" 866 | description = "Python port of markdown-it. Markdown parsing, done right!" 867 | optional = false 868 | python-versions = ">=3.8" 869 | files = [ 870 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 871 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 872 | ] 873 | 874 | [package.dependencies] 875 | mdurl = ">=0.1,<1.0" 876 | 877 | [package.extras] 878 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 879 | code-style = ["pre-commit (>=3.0,<4.0)"] 880 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 881 | linkify = ["linkify-it-py (>=1,<3)"] 882 | plugins = ["mdit-py-plugins"] 883 | profiling = ["gprof2dot"] 884 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 885 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 886 | 887 | [[package]] 888 | name = "mccabe" 889 | version = "0.7.0" 890 | description = "McCabe checker, plugin for flake8" 891 | optional = false 892 | python-versions = ">=3.6" 893 | files = [ 894 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 895 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 896 | ] 897 | 898 | [[package]] 899 | name = "mdurl" 900 | version = "0.1.2" 901 | description = "Markdown URL utilities" 902 | optional = false 903 | python-versions = ">=3.7" 904 | files = [ 905 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 906 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 907 | ] 908 | 909 | [[package]] 910 | name = "msgpack" 911 | version = "1.1.0" 912 | description = "MessagePack serializer" 913 | optional = false 914 | python-versions = ">=3.8" 915 | files = [ 916 | {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, 917 | {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, 918 | {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, 919 | {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, 920 | {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, 921 | {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, 922 | {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, 923 | {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, 924 | {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, 925 | {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, 926 | {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, 927 | {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, 928 | {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, 929 | {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, 930 | {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, 931 | {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, 932 | {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, 933 | {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, 934 | {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, 935 | {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, 936 | {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, 937 | {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, 938 | {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, 939 | {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, 940 | {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, 941 | {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, 942 | {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, 943 | {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, 944 | {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, 945 | {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, 946 | {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, 947 | {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, 948 | {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, 949 | {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, 950 | {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, 951 | {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, 952 | {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, 953 | {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, 954 | {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, 955 | {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, 956 | {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, 957 | {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, 958 | {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, 959 | {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, 960 | {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, 961 | {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, 962 | {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, 963 | {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, 964 | {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, 965 | {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, 966 | {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, 967 | {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, 968 | {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, 969 | {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, 970 | {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, 971 | {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, 972 | {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, 973 | {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, 974 | {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, 975 | {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, 976 | {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, 977 | {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, 978 | {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, 979 | {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, 980 | ] 981 | 982 | [[package]] 983 | name = "mypy-extensions" 984 | version = "1.0.0" 985 | description = "Type system extensions for programs checked with the mypy type checker." 986 | optional = false 987 | python-versions = ">=3.5" 988 | files = [ 989 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 990 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 991 | ] 992 | 993 | [[package]] 994 | name = "nodeenv" 995 | version = "1.9.1" 996 | description = "Node.js virtual environment builder" 997 | optional = false 998 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 999 | files = [ 1000 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 1001 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "packageurl-python" 1006 | version = "0.16.0" 1007 | description = "A purl aka. Package URL parser and builder" 1008 | optional = false 1009 | python-versions = ">=3.8" 1010 | files = [ 1011 | {file = "packageurl_python-0.16.0-py3-none-any.whl", hash = "sha256:5c3872638b177b0f1cf01c3673017b7b27ebee485693ae12a8bed70fa7fa7c35"}, 1012 | {file = "packageurl_python-0.16.0.tar.gz", hash = "sha256:69e3bf8a3932fe9c2400f56aaeb9f86911ecee2f9398dbe1b58ec34340be365d"}, 1013 | ] 1014 | 1015 | [package.extras] 1016 | build = ["setuptools", "wheel"] 1017 | lint = ["black", "isort", "mypy"] 1018 | sqlalchemy = ["sqlalchemy (>=2.0.0)"] 1019 | test = ["pytest"] 1020 | 1021 | [[package]] 1022 | name = "packaging" 1023 | version = "24.2" 1024 | description = "Core utilities for Python packages" 1025 | optional = false 1026 | python-versions = ">=3.8" 1027 | files = [ 1028 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 1029 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "pathspec" 1034 | version = "0.12.1" 1035 | description = "Utility library for gitignore style pattern matching of file paths." 1036 | optional = false 1037 | python-versions = ">=3.8" 1038 | files = [ 1039 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 1040 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "pip" 1045 | version = "24.3.1" 1046 | description = "The PyPA recommended tool for installing Python packages." 1047 | optional = false 1048 | python-versions = ">=3.8" 1049 | files = [ 1050 | {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, 1051 | {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "pip-api" 1056 | version = "0.0.34" 1057 | description = "An unofficial, importable pip API" 1058 | optional = false 1059 | python-versions = ">=3.8" 1060 | files = [ 1061 | {file = "pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb"}, 1062 | {file = "pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625"}, 1063 | ] 1064 | 1065 | [package.dependencies] 1066 | pip = "*" 1067 | 1068 | [[package]] 1069 | name = "pip-audit" 1070 | version = "2.7.1" 1071 | description = "A tool for scanning Python environments for known vulnerabilities" 1072 | optional = false 1073 | python-versions = ">=3.8" 1074 | files = [ 1075 | {file = "pip_audit-2.7.1-py3-none-any.whl", hash = "sha256:b9b4230d1ac685d669b4a36b1d5f849ea3d1ce371501aff73047bd278b22c055"}, 1076 | {file = "pip_audit-2.7.1.tar.gz", hash = "sha256:66001c73bc6e5ebc998ef31a32432f7b479dc3bfeb40f7101d0fe7eb564a2c2a"}, 1077 | ] 1078 | 1079 | [package.dependencies] 1080 | CacheControl = {version = ">=0.13.0", extras = ["filecache"]} 1081 | cyclonedx-python-lib = ">=5,<7" 1082 | html5lib = ">=1.1" 1083 | packaging = ">=23.0.0" 1084 | pip-api = ">=0.0.28" 1085 | pip-requirements-parser = ">=32.0.0" 1086 | requests = ">=2.31.0" 1087 | rich = ">=12.4" 1088 | toml = ">=0.10" 1089 | 1090 | [package.extras] 1091 | dev = ["build", "bump (>=1.3.2)", "pip-audit[doc,lint,test]"] 1092 | doc = ["pdoc"] 1093 | lint = ["interrogate", "mypy", "ruff (<0.2.2)", "types-html5lib", "types-requests", "types-toml"] 1094 | test = ["coverage[toml] (>=7.0,!=7.3.3,<8.0)", "pretend", "pytest", "pytest-cov"] 1095 | 1096 | [[package]] 1097 | name = "pip-requirements-parser" 1098 | version = "32.0.1" 1099 | description = "pip requirements parser - a mostly correct pip requirements parsing library because it uses pip's own code." 1100 | optional = false 1101 | python-versions = ">=3.6.0" 1102 | files = [ 1103 | {file = "pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3"}, 1104 | {file = "pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526"}, 1105 | ] 1106 | 1107 | [package.dependencies] 1108 | packaging = "*" 1109 | pyparsing = "*" 1110 | 1111 | [package.extras] 1112 | docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)"] 1113 | testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)"] 1114 | 1115 | [[package]] 1116 | name = "platformdirs" 1117 | version = "4.3.6" 1118 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 1119 | optional = false 1120 | python-versions = ">=3.8" 1121 | files = [ 1122 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 1123 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 1124 | ] 1125 | 1126 | [package.extras] 1127 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 1128 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 1129 | type = ["mypy (>=1.11.2)"] 1130 | 1131 | [[package]] 1132 | name = "pluggy" 1133 | version = "1.5.0" 1134 | description = "plugin and hook calling mechanisms for python" 1135 | optional = false 1136 | python-versions = ">=3.8" 1137 | files = [ 1138 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 1139 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 1140 | ] 1141 | 1142 | [package.extras] 1143 | dev = ["pre-commit", "tox"] 1144 | testing = ["pytest", "pytest-benchmark"] 1145 | 1146 | [[package]] 1147 | name = "pre-commit" 1148 | version = "3.6.2" 1149 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 1150 | optional = false 1151 | python-versions = ">=3.9" 1152 | files = [ 1153 | {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, 1154 | {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, 1155 | ] 1156 | 1157 | [package.dependencies] 1158 | cfgv = ">=2.0.0" 1159 | identify = ">=1.0.0" 1160 | nodeenv = ">=0.11.1" 1161 | pyyaml = ">=5.1" 1162 | virtualenv = ">=20.10.0" 1163 | 1164 | [[package]] 1165 | name = "publication" 1166 | version = "0.0.3" 1167 | description = "Publication helps you maintain public-api-friendly modules by preventing unintentional access to private implementation details via introspection." 1168 | optional = false 1169 | python-versions = "*" 1170 | files = [ 1171 | {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, 1172 | {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "py-serializable" 1177 | version = "1.1.2" 1178 | description = "Library for serializing and deserializing Python Objects to and from JSON and XML." 1179 | optional = false 1180 | python-versions = "<4.0,>=3.8" 1181 | files = [ 1182 | {file = "py_serializable-1.1.2-py3-none-any.whl", hash = "sha256:801be61b0a1ba64c3861f7c624f1de5cfbbabf8b458acc9cdda91e8f7e5effa1"}, 1183 | {file = "py_serializable-1.1.2.tar.gz", hash = "sha256:89af30bc319047d4aa0d8708af412f6ce73835e18bacf1a080028bb9e2f42bdb"}, 1184 | ] 1185 | 1186 | [package.dependencies] 1187 | defusedxml = ">=0.7.1,<0.8.0" 1188 | 1189 | [[package]] 1190 | name = "pycodestyle" 1191 | version = "2.12.1" 1192 | description = "Python style guide checker" 1193 | optional = false 1194 | python-versions = ">=3.8" 1195 | files = [ 1196 | {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, 1197 | {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "pycparser" 1202 | version = "2.22" 1203 | description = "C parser in Python" 1204 | optional = false 1205 | python-versions = ">=3.8" 1206 | files = [ 1207 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 1208 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "pyflakes" 1213 | version = "3.2.0" 1214 | description = "passive checker of Python programs" 1215 | optional = false 1216 | python-versions = ">=3.8" 1217 | files = [ 1218 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 1219 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "pygit2" 1224 | version = "1.16.0" 1225 | description = "Python bindings for libgit2." 1226 | optional = false 1227 | python-versions = ">=3.10" 1228 | files = [ 1229 | {file = "pygit2-1.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d82428a1757b5b9708b3402197142c42d327880cd2413d11dc2c188f7c7bb561"}, 1230 | {file = "pygit2-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cb1a1e62195e47b05bf7fe2bf96b28a4b8ccf726aa76392f36927b6074b10bc"}, 1231 | {file = "pygit2-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0ea2a546af9840ff008a04631215b3dddebc39722edaf9e0db11e3ed2fefa95"}, 1232 | {file = "pygit2-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba530d82c3bb9bdf17716cff3bda06ac8730f1cc337fccb5d5f864870a7bd221"}, 1233 | {file = "pygit2-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:17464547dda63e67228f56f714f6c4593932d5209472afcecad22017e21b746e"}, 1234 | {file = "pygit2-1.16.0-cp310-cp310-win32.whl", hash = "sha256:6526c82ebfad5b138d3d14419664a2763f2d0fc060018b274acd564001a381e6"}, 1235 | {file = "pygit2-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0a414dbc8de151df9b9a9e14bdf236d22aa0cbe63d54768aa8321ea4bb839e76"}, 1236 | {file = "pygit2-1.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f472cdb2c4ca4114c9d456e8731298771557c146eeb746e033d9a58d58cf8bf9"}, 1237 | {file = "pygit2-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:676b18530dd3fe87eb3a6aef76417eeff5c25aebcbf04877329dedd31999bbe5"}, 1238 | {file = "pygit2-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31ab0a3069409ab7e8e1cfac98428295f2319bbbbdab1a40ff6243452b66a71a"}, 1239 | {file = "pygit2-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93349517205f3230e12ac6bab395a55c7441a50fbd0f3892c74a400697422d45"}, 1240 | {file = "pygit2-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:69d83cf79672c3f70e146622d3416acfc6b1f1a9c65f67b95b588aba350fddbc"}, 1241 | {file = "pygit2-1.16.0-cp311-cp311-win32.whl", hash = "sha256:1c29937f799347031d299e26fa0ce1fc5cd6ed9b244a0f7cfb7ce526217cd39b"}, 1242 | {file = "pygit2-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14b1d2d0b9d1480a9afc1c06b7ed580ec8f6cf565a06d70d49749987b9cd9d4"}, 1243 | {file = "pygit2-1.16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9a51f7594c15a2c628268f2c9fef795c14669974cfc15fcb58b74e08d917b0db"}, 1244 | {file = "pygit2-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4822c5ea9e25c2efad9724d3663be50aca7a5d81b908befe0afde462ed75b10"}, 1245 | {file = "pygit2-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:588b385139e9f2ba789ec5462683141bc72a4729e7c9f07a6d525f3f40b36258"}, 1246 | {file = "pygit2-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e70c7f3315d957c8e8fe8e53129706e1096d96eaa91129a706b7324babda92"}, 1247 | {file = "pygit2-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84b4f681576aeef63fcb195cc7f39a927e837d143e3f1a8f43333440a6cad939"}, 1248 | {file = "pygit2-1.16.0-cp312-cp312-win32.whl", hash = "sha256:d475c40eab4996f4255ed4efe5ffe2fa42112e1557300c07752726d308c68952"}, 1249 | {file = "pygit2-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:d3d049872a58834a3ac3fb33606ca7fc1f95b1baf34f66cde16927fd7e3f88df"}, 1250 | {file = "pygit2-1.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a382a6fd02bbbaff26afba722776b099d190c51cc94812b506f4f97c50a787ab"}, 1251 | {file = "pygit2-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41db6402f29d2a79f5a39fb777c50b101f63f7a2e0c6d92e9c67685dd969ea82"}, 1252 | {file = "pygit2-1.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:111f22f9e02b9cd353f150fe32d8232b76129de6546d92d50e45a204bfcf5921"}, 1253 | {file = "pygit2-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e544df34c3ad768332837a987a2814ca9d8f0902243bf0e5a10439b7367b8f76"}, 1254 | {file = "pygit2-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b0a85ddcd2c93e1cd4080c24a1908711e2ba91359fb8fecc17dc5f304bb5599"}, 1255 | {file = "pygit2-1.16.0-cp313-cp313-win32.whl", hash = "sha256:b9fbd7dc9b1fd1d5d41be92079a9b88d7f9f76552890eb46bfb22a592fb0bb1e"}, 1256 | {file = "pygit2-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:feba68ca3ef1848b61f9254785aafda74f59170050201adb3844fd2b46dbe2d6"}, 1257 | {file = "pygit2-1.16.0.tar.gz", hash = "sha256:7b29a6796baa15fc89d443ac8d51775411d9b1e5b06dc40d458c56c8576b48a2"}, 1258 | ] 1259 | 1260 | [package.dependencies] 1261 | cffi = ">=1.17.0" 1262 | 1263 | [[package]] 1264 | name = "pygments" 1265 | version = "2.18.0" 1266 | description = "Pygments is a syntax highlighting package written in Python." 1267 | optional = false 1268 | python-versions = ">=3.8" 1269 | files = [ 1270 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 1271 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 1272 | ] 1273 | 1274 | [package.extras] 1275 | windows-terminal = ["colorama (>=0.4.6)"] 1276 | 1277 | [[package]] 1278 | name = "pylint" 1279 | version = "3.3.0" 1280 | description = "python code static checker" 1281 | optional = false 1282 | python-versions = ">=3.9.0" 1283 | files = [ 1284 | {file = "pylint-3.3.0-py3-none-any.whl", hash = "sha256:02dce1845f68974b9b03045894eb3bf05a8b3c7da9fd10af4de3c91e69eb92f1"}, 1285 | {file = "pylint-3.3.0.tar.gz", hash = "sha256:c685fe3c061ee5fb0ce7c29436174ab84a2f525fce2a268b1986e921e083fe22"}, 1286 | ] 1287 | 1288 | [package.dependencies] 1289 | astroid = ">=3.3.3,<=3.4.0-dev0" 1290 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 1291 | dill = [ 1292 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 1293 | {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, 1294 | {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, 1295 | ] 1296 | isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" 1297 | mccabe = ">=0.6,<0.8" 1298 | platformdirs = ">=2.2.0" 1299 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 1300 | tomlkit = ">=0.10.1" 1301 | 1302 | [package.extras] 1303 | spelling = ["pyenchant (>=3.2,<4.0)"] 1304 | testutils = ["gitpython (>3)"] 1305 | 1306 | [[package]] 1307 | name = "pyparsing" 1308 | version = "3.2.1" 1309 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 1310 | optional = false 1311 | python-versions = ">=3.9" 1312 | files = [ 1313 | {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, 1314 | {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, 1315 | ] 1316 | 1317 | [package.extras] 1318 | diagrams = ["jinja2", "railroad-diagrams"] 1319 | 1320 | [[package]] 1321 | name = "pyproject-api" 1322 | version = "1.8.0" 1323 | description = "API to interact with the python pyproject.toml based projects" 1324 | optional = false 1325 | python-versions = ">=3.8" 1326 | files = [ 1327 | {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, 1328 | {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, 1329 | ] 1330 | 1331 | [package.dependencies] 1332 | packaging = ">=24.1" 1333 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 1334 | 1335 | [package.extras] 1336 | docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"] 1337 | testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] 1338 | 1339 | [[package]] 1340 | name = "pytest" 1341 | version = "8.0.2" 1342 | description = "pytest: simple powerful testing with Python" 1343 | optional = false 1344 | python-versions = ">=3.8" 1345 | files = [ 1346 | {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, 1347 | {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, 1348 | ] 1349 | 1350 | [package.dependencies] 1351 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1352 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 1353 | iniconfig = "*" 1354 | packaging = "*" 1355 | pluggy = ">=1.3.0,<2.0" 1356 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 1357 | 1358 | [package.extras] 1359 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 1360 | 1361 | [[package]] 1362 | name = "pytest-cov" 1363 | version = "4.1.0" 1364 | description = "Pytest plugin for measuring coverage." 1365 | optional = false 1366 | python-versions = ">=3.7" 1367 | files = [ 1368 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 1369 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 1370 | ] 1371 | 1372 | [package.dependencies] 1373 | coverage = {version = ">=5.2.1", extras = ["toml"]} 1374 | pytest = ">=4.6" 1375 | 1376 | [package.extras] 1377 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1378 | 1379 | [[package]] 1380 | name = "pytest-env" 1381 | version = "1.1.3" 1382 | description = "pytest plugin that allows you to add environment variables." 1383 | optional = false 1384 | python-versions = ">=3.8" 1385 | files = [ 1386 | {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, 1387 | {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, 1388 | ] 1389 | 1390 | [package.dependencies] 1391 | pytest = ">=7.4.3" 1392 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 1393 | 1394 | [package.extras] 1395 | test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] 1396 | 1397 | [[package]] 1398 | name = "python-dateutil" 1399 | version = "2.9.0.post0" 1400 | description = "Extensions to the standard Python datetime module" 1401 | optional = false 1402 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1403 | files = [ 1404 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 1405 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 1406 | ] 1407 | 1408 | [package.dependencies] 1409 | six = ">=1.5" 1410 | 1411 | [[package]] 1412 | name = "pyyaml" 1413 | version = "6.0.2" 1414 | description = "YAML parser and emitter for Python" 1415 | optional = false 1416 | python-versions = ">=3.8" 1417 | files = [ 1418 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 1419 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 1420 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 1421 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 1422 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 1423 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 1424 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 1425 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 1426 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 1427 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 1428 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 1429 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 1430 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 1431 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 1432 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 1433 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 1434 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 1435 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 1436 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 1437 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 1438 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 1439 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 1440 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 1441 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 1442 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 1443 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 1444 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 1445 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 1446 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 1447 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 1448 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 1449 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 1450 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 1451 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 1452 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 1453 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 1454 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 1455 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 1456 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 1457 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 1458 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 1459 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 1460 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 1461 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 1462 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 1463 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 1464 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 1465 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 1466 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 1467 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 1468 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 1469 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 1470 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "requests" 1475 | version = "2.32.3" 1476 | description = "Python HTTP for Humans." 1477 | optional = false 1478 | python-versions = ">=3.8" 1479 | files = [ 1480 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 1481 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 1482 | ] 1483 | 1484 | [package.dependencies] 1485 | certifi = ">=2017.4.17" 1486 | charset-normalizer = ">=2,<4" 1487 | idna = ">=2.5,<4" 1488 | urllib3 = ">=1.21.1,<3" 1489 | 1490 | [package.extras] 1491 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1492 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1493 | 1494 | [[package]] 1495 | name = "rich" 1496 | version = "13.9.4" 1497 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 1498 | optional = false 1499 | python-versions = ">=3.8.0" 1500 | files = [ 1501 | {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, 1502 | {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, 1503 | ] 1504 | 1505 | [package.dependencies] 1506 | markdown-it-py = ">=2.2.0" 1507 | pygments = ">=2.13.0,<3.0.0" 1508 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} 1509 | 1510 | [package.extras] 1511 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 1512 | 1513 | [[package]] 1514 | name = "s3transfer" 1515 | version = "0.10.4" 1516 | description = "An Amazon S3 Transfer Manager" 1517 | optional = false 1518 | python-versions = ">=3.8" 1519 | files = [ 1520 | {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, 1521 | {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, 1522 | ] 1523 | 1524 | [package.dependencies] 1525 | botocore = ">=1.33.2,<2.0a.0" 1526 | 1527 | [package.extras] 1528 | crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] 1529 | 1530 | [[package]] 1531 | name = "six" 1532 | version = "1.17.0" 1533 | description = "Python 2 and 3 compatibility utilities" 1534 | optional = false 1535 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1536 | files = [ 1537 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 1538 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 1539 | ] 1540 | 1541 | [[package]] 1542 | name = "sortedcontainers" 1543 | version = "2.4.0" 1544 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 1545 | optional = false 1546 | python-versions = "*" 1547 | files = [ 1548 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1549 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "toml" 1554 | version = "0.10.2" 1555 | description = "Python Library for Tom's Obvious, Minimal Language" 1556 | optional = false 1557 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1558 | files = [ 1559 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1560 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1561 | ] 1562 | 1563 | [[package]] 1564 | name = "tomli" 1565 | version = "2.2.1" 1566 | description = "A lil' TOML parser" 1567 | optional = false 1568 | python-versions = ">=3.8" 1569 | files = [ 1570 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 1571 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 1572 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 1573 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 1574 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 1575 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 1576 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 1577 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 1578 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 1579 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 1580 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 1581 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 1582 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 1583 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 1584 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 1585 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 1586 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 1587 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 1588 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 1589 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 1590 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 1591 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 1592 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 1593 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 1594 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 1595 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 1596 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 1597 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 1598 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 1599 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 1600 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 1601 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 1602 | ] 1603 | 1604 | [[package]] 1605 | name = "tomlkit" 1606 | version = "0.13.2" 1607 | description = "Style preserving TOML library" 1608 | optional = false 1609 | python-versions = ">=3.8" 1610 | files = [ 1611 | {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, 1612 | {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, 1613 | ] 1614 | 1615 | [[package]] 1616 | name = "tox" 1617 | version = "4.23.2" 1618 | description = "tox is a generic virtualenv management and test command line tool" 1619 | optional = false 1620 | python-versions = ">=3.8" 1621 | files = [ 1622 | {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, 1623 | {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, 1624 | ] 1625 | 1626 | [package.dependencies] 1627 | cachetools = ">=5.5" 1628 | chardet = ">=5.2" 1629 | colorama = ">=0.4.6" 1630 | filelock = ">=3.16.1" 1631 | packaging = ">=24.1" 1632 | platformdirs = ">=4.3.6" 1633 | pluggy = ">=1.5" 1634 | pyproject-api = ">=1.8" 1635 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 1636 | typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} 1637 | virtualenv = ">=20.26.6" 1638 | 1639 | [package.extras] 1640 | test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] 1641 | 1642 | [[package]] 1643 | name = "typeguard" 1644 | version = "2.13.3" 1645 | description = "Run-time type checker for Python" 1646 | optional = false 1647 | python-versions = ">=3.5.3" 1648 | files = [ 1649 | {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, 1650 | {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, 1651 | ] 1652 | 1653 | [package.extras] 1654 | doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 1655 | test = ["mypy", "pytest", "typing-extensions"] 1656 | 1657 | [[package]] 1658 | name = "typing-extensions" 1659 | version = "4.12.2" 1660 | description = "Backported and Experimental Type Hints for Python 3.8+" 1661 | optional = false 1662 | python-versions = ">=3.8" 1663 | files = [ 1664 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 1665 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "urllib3" 1670 | version = "2.3.0" 1671 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1672 | optional = false 1673 | python-versions = ">=3.9" 1674 | files = [ 1675 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 1676 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 1677 | ] 1678 | 1679 | [package.extras] 1680 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1681 | h2 = ["h2 (>=4,<5)"] 1682 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1683 | zstd = ["zstandard (>=0.18.0)"] 1684 | 1685 | [[package]] 1686 | name = "virtualenv" 1687 | version = "20.28.1" 1688 | description = "Virtual Python Environment builder" 1689 | optional = false 1690 | python-versions = ">=3.8" 1691 | files = [ 1692 | {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, 1693 | {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, 1694 | ] 1695 | 1696 | [package.dependencies] 1697 | distlib = ">=0.3.7,<1" 1698 | filelock = ">=3.12.2,<4" 1699 | platformdirs = ">=3.9.1,<5" 1700 | 1701 | [package.extras] 1702 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1703 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1704 | 1705 | [[package]] 1706 | name = "webencodings" 1707 | version = "0.5.1" 1708 | description = "Character encoding aliases for legacy web content" 1709 | optional = false 1710 | python-versions = "*" 1711 | files = [ 1712 | {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, 1713 | {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, 1714 | ] 1715 | 1716 | [metadata] 1717 | lock-version = "2.0" 1718 | python-versions = ">=3.10,<4.0" 1719 | content-hash = "c8039a73e0b4f962e2977d81665d0ca24f1a5fc1baed150bb52404e2dfa584cf" 1720 | --------------------------------------------------------------------------------