From 17496635beb858a441ab2b9669bcf03b703b7a5e Mon Sep 17 00:00:00 2001 From: Ravi Jadhav Date: Mon, 13 Feb 2023 20:19:53 -0800 Subject: [PATCH] feat: Added uploadAssetWorkflow lambda function 1. Workflow currently only includes asset upload 2. added tests --- README.md | 3 +- backend/.flake8 | 2 +- .../functions/assets/uploadAssetWorkflow.py | 14 - .../assets/upload_asset_workflow/__init__.py | 0 .../upload_asset_workflow/lambda_handler.py | 32 + .../upload_asset_workflow/request_handler.py | 27 + backend/backend/logging/logger.py | 34 + backend/backend/models/__init__.py | 0 backend/backend/models/assets.py | 52 ++ backend/backend/models/common.py | 43 ++ backend/poetry.lock | 611 +++++++++++++++++- backend/pyproject.toml | 4 + backend/requirements.txt | 59 +- backend/tests/functions/__init__.py | 0 backend/tests/functions/assets/__init__.py | 0 .../assets/upload_asset_workflow/__init__.py | 0 .../test_lambda_handler.py | 79 +++ .../test_request_handler.py | 56 ++ backend/tests/models/__init__.py | 0 backend/tests/models/assets/__init__.py | 0 backend/tests/models/assets/test_common.py | 33 + backend/tests/test_sample.py | 10 - infra/lib/api-builder.ts | 2 +- infra/lib/lambdaBuilder/assetFunctions.ts | 7 +- web/logo_dark.png | Bin 0 -> 10577 bytes 25 files changed, 1013 insertions(+), 55 deletions(-) delete mode 100644 backend/backend/functions/assets/uploadAssetWorkflow.py create mode 100644 backend/backend/functions/assets/upload_asset_workflow/__init__.py create mode 100644 backend/backend/functions/assets/upload_asset_workflow/lambda_handler.py create mode 100644 backend/backend/functions/assets/upload_asset_workflow/request_handler.py create mode 100644 backend/backend/logging/logger.py create mode 100644 backend/backend/models/__init__.py create mode 100644 backend/backend/models/assets.py create mode 100644 backend/backend/models/common.py create mode 100644 backend/tests/functions/__init__.py create mode 100644 backend/tests/functions/assets/__init__.py create mode 100644 backend/tests/functions/assets/upload_asset_workflow/__init__.py create mode 100644 backend/tests/functions/assets/upload_asset_workflow/test_lambda_handler.py create mode 100644 backend/tests/functions/assets/upload_asset_workflow/test_request_handler.py create mode 100644 backend/tests/models/__init__.py create mode 100644 backend/tests/models/assets/__init__.py create mode 100644 backend/tests/models/assets/test_common.py delete mode 100644 backend/tests/test_sample.py create mode 100644 web/logo_dark.png diff --git a/README.md b/README.md index 7df1f23b..ef4650cf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Visual Asset Management System(VAMS) -![Logo](./web/logo_white.png) +![Logo](./web/logo_dark.png#gh-light-mode-only) +![Logo](./web/logo_white.png#gh-dark-mode-only) ![Build](https://github.com/awslabs/visual-asset-management-system/actions/workflows/ci.yml/badge.svg) ## Introduction diff --git a/backend/.flake8 b/backend/.flake8 index 85517caa..ba0cc4ba 100644 --- a/backend/.flake8 +++ b/backend/.flake8 @@ -1,5 +1,5 @@ [flake8] -max-line-length = 88 +max-line-length = 120 extend-ignore = E203 exclude = ./backend/common/* diff --git a/backend/backend/functions/assets/uploadAssetWorkflow.py b/backend/backend/functions/assets/uploadAssetWorkflow.py deleted file mode 100644 index dab4da9b..00000000 --- a/backend/backend/functions/assets/uploadAssetWorkflow.py +++ /dev/null @@ -1,14 +0,0 @@ - -from typing import Any, Dict -from aws_lambda_powertools import Logger -from aws_lambda_powertools.utilities.typing import LambdaContext - -logger = Logger() - - -def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None: - logger.info("Starting uploadAssetWorkflow") - - -def add(a: int, b: int) -> int: - return a+b diff --git a/backend/backend/functions/assets/upload_asset_workflow/__init__.py b/backend/backend/functions/assets/upload_asset_workflow/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/backend/functions/assets/upload_asset_workflow/lambda_handler.py b/backend/backend/functions/assets/upload_asset_workflow/lambda_handler.py new file mode 100644 index 00000000..cd3898d5 --- /dev/null +++ b/backend/backend/functions/assets/upload_asset_workflow/lambda_handler.py @@ -0,0 +1,32 @@ +import os +from typing import Any, Dict +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.parser import parse, ValidationError +from backend.functions.assets.upload_asset_workflow.request_handler import UploadAssetWorkflowRequestHandler +from backend.logging.logger import safeLogger + +from backend.models.assets import UploadAssetWorkflowRequestModel +from backend.models.common import APIGatewayProxyResponseV2, internal_error, success, validation_error +import boto3 + +logger = safeLogger(service_name="UploadAssetWorkflow") +handler = UploadAssetWorkflowRequestHandler( + sfn_client=boto3.client('stepfunctions'), + state_machine_arn=os.environ["UPLOAD_WORKFLOW_ARN"] +) + + +def lambda_handler(event: Dict[Any, Any], context: LambdaContext) -> APIGatewayProxyResponseV2: + try: + request = parse(event['body'], model=UploadAssetWorkflowRequestModel) + logger.info(request) + response = handler.process_request(request=request) + return success(body=response.dict()) + except ValidationError as v: + logger.exception("ValidationError") + return validation_error(body={ + 'message': str(v) + }) + except Exception as e: + logger.exception("Exception") + return internal_error(body={'message': str(e)}) diff --git a/backend/backend/functions/assets/upload_asset_workflow/request_handler.py b/backend/backend/functions/assets/upload_asset_workflow/request_handler.py new file mode 100644 index 00000000..137e6096 --- /dev/null +++ b/backend/backend/functions/assets/upload_asset_workflow/request_handler.py @@ -0,0 +1,27 @@ +import json +from backend.logging.logger import safeLogger +from backend.models.assets import ( + GetUploadAssetWorkflowStepFunctionInput, + UploadAssetWorkflowRequestModel, + UploadAssetWorkflowResponseModel, +) +from mypy_boto3_stepfunctions import Client + +logger = safeLogger(child=True) + + +class UploadAssetWorkflowRequestHandler: + + def __init__(self, sfn_client: Client, state_machine_arn: str) -> None: + self.sfn_client = sfn_client + self.stat_machine_arn = state_machine_arn + + def process_request(self, request: UploadAssetWorkflowRequestModel) -> UploadAssetWorkflowResponseModel: + stepfunction_request = GetUploadAssetWorkflowStepFunctionInput(request) + sfn_response = self.sfn_client.start_execution( + stateMachineArn=self.stat_machine_arn, + input=json.dumps(stepfunction_request.dict()) + ) + logger.info(f"Started uploadAssetWorkflow: Execution Id: {sfn_response['executionArn']}") + response: UploadAssetWorkflowResponseModel = UploadAssetWorkflowResponseModel(message='Success') + return response diff --git a/backend/backend/logging/logger.py b/backend/backend/logging/logger.py new file mode 100644 index 00000000..28b04078 --- /dev/null +++ b/backend/backend/logging/logger.py @@ -0,0 +1,34 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.logging.formatter import LambdaPowertoolsFormatter + +location_format = "[%(funcName)s] %(module)s" +date_format = "%m/%d/%Y %I:%M:%S %p" + + +def mask_sensitive_data(event): + # remove sensitive data from request object before logging + keys_to_redact = ["authorization"] + result = {} + for k, v in event.items(): + if isinstance(v, dict): + result[k] = mask_sensitive_data(v) + elif k in keys_to_redact: + result[k] = "" + else: + result[k] = v + return result + + +class CustomFormatter(LambdaPowertoolsFormatter): + def serialize(self, log: dict) -> str: + """Serialize final structured log dict to JSON str""" + log = mask_sensitive_data(event=log) # rename message key to event + return self.json_serializer(log) # use configured json serializer + + +def safeLogger(**kwargs): + return Logger( + logger_formatter=CustomFormatter(), + location=location_format, + datefmt=date_format, + **kwargs) diff --git a/backend/backend/models/__init__.py b/backend/backend/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/backend/models/assets.py b/backend/backend/models/assets.py new file mode 100644 index 00000000..9810eacc --- /dev/null +++ b/backend/backend/models/assets.py @@ -0,0 +1,52 @@ +from pydantic import BaseModel, Json +from aws_lambda_powertools.utilities.parser.models import ( + APIGatewayProxyEventV2Model +) + + +class AssetPreviewLocationModel(BaseModel): + Bucket: str + Key: str + + +class UploadAssetModel(BaseModel): + databaseId: str + assetId: str + bucket: str + key: str + assetType: str + description: str + isDistributable: bool + Comment: str + previewLocation: AssetPreviewLocationModel + specifiedPipelines: list[str] + + +class UploadAssetWorkflowRequestModel(BaseModel): + uploadAssetBody: UploadAssetModel + + +class UploadAssetWorkflowResponseModel(BaseModel): + message: str + + +class UploadAssetWorkflowRequest(APIGatewayProxyEventV2Model): + body: Json[UploadAssetWorkflowRequestModel] # type: ignore[assignment] + + +class UploadAssetStepFunctionRequest(BaseModel): + body: UploadAssetModel + + +class UploadAssetWorkflowStepFunctionInput(BaseModel): + uploadAssetBody: UploadAssetStepFunctionRequest + + +def GetUploadAssetWorkflowStepFunctionInput( + uploadAssetWorkflowRequestModel: UploadAssetWorkflowRequestModel +) -> UploadAssetWorkflowStepFunctionInput: + return UploadAssetWorkflowStepFunctionInput( + uploadAssetBody=UploadAssetStepFunctionRequest( + body=uploadAssetWorkflowRequestModel.uploadAssetBody + ) + ) diff --git a/backend/backend/models/common.py b/backend/backend/models/common.py new file mode 100644 index 00000000..c0caeac8 --- /dev/null +++ b/backend/backend/models/common.py @@ -0,0 +1,43 @@ +import json +from typing import Any, Dict, TypedDict + + +class APIGatewayProxyResponseV2(TypedDict): + isBase64Encoded: bool + statusCode: int + headers: Dict[str, str] + body: str + + +def commonHeaders() -> Dict[str, str]: + return { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + + +def success(status_code: int = 200, body: Any = {'message': 'Success'}) -> APIGatewayProxyResponseV2: + return APIGatewayProxyResponseV2( + isBase64Encoded=False, + statusCode=status_code, + headers=commonHeaders(), + body=json.dumps(body) + ) + + +def validation_error(status_code: int = 422, body: dict = {'message': 'Validation Error'}) -> APIGatewayProxyResponseV2: + return APIGatewayProxyResponseV2( + isBase64Encoded=False, + statusCode=status_code, + headers=commonHeaders(), + body=json.dumps(body) + ) + + +def internal_error(status_code: int = 500, body: Any = {'message': 'Validation Error'}) -> APIGatewayProxyResponseV2: + return APIGatewayProxyResponseV2( + isBase64Encoded=False, + statusCode=status_code, + headers=commonHeaders(), + body=json.dumps(body) + ) diff --git a/backend/poetry.lock b/backend/poetry.lock index 70e111aa..4f61a1e4 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -54,6 +54,364 @@ s3transfer = ">=0.6.0,<0.7.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] +[[package]] +name = "boto3-stubs" +version = "1.26.69" +description = "Type annotations for boto3 1.26.69 generated with mypy-boto3-builder 7.12.3" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +botocore-stubs = "*" +mypy-boto3-stepfunctions = {version = ">=1.26.0,<1.27.0", optional = true, markers = "extra == \"stepfunctions\""} +types-s3transfer = "*" +typing-extensions = ">=4.1.0" + +[package.extras] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.26.0,<1.27.0)"] +account = ["mypy-boto3-account (>=1.26.0,<1.27.0)"] +acm = ["mypy-boto3-acm (>=1.26.0,<1.27.0)"] +acm-pca = ["mypy-boto3-acm-pca (>=1.26.0,<1.27.0)"] +alexaforbusiness = ["mypy-boto3-alexaforbusiness (>=1.26.0,<1.27.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.26.0,<1.27.0)", "mypy-boto3-account (>=1.26.0,<1.27.0)", "mypy-boto3-acm (>=1.26.0,<1.27.0)", "mypy-boto3-acm-pca (>=1.26.0,<1.27.0)", "mypy-boto3-alexaforbusiness (>=1.26.0,<1.27.0)", "mypy-boto3-amp (>=1.26.0,<1.27.0)", "mypy-boto3-amplify (>=1.26.0,<1.27.0)", "mypy-boto3-amplifybackend (>=1.26.0,<1.27.0)", "mypy-boto3-amplifyuibuilder (>=1.26.0,<1.27.0)", "mypy-boto3-apigateway (>=1.26.0,<1.27.0)", "mypy-boto3-apigatewaymanagementapi (>=1.26.0,<1.27.0)", "mypy-boto3-apigatewayv2 (>=1.26.0,<1.27.0)", "mypy-boto3-appconfig (>=1.26.0,<1.27.0)", "mypy-boto3-appconfigdata (>=1.26.0,<1.27.0)", "mypy-boto3-appflow (>=1.26.0,<1.27.0)", "mypy-boto3-appintegrations (>=1.26.0,<1.27.0)", "mypy-boto3-application-autoscaling (>=1.26.0,<1.27.0)", "mypy-boto3-application-insights (>=1.26.0,<1.27.0)", "mypy-boto3-applicationcostprofiler (>=1.26.0,<1.27.0)", "mypy-boto3-appmesh (>=1.26.0,<1.27.0)", "mypy-boto3-apprunner (>=1.26.0,<1.27.0)", "mypy-boto3-appstream (>=1.26.0,<1.27.0)", "mypy-boto3-appsync (>=1.26.0,<1.27.0)", "mypy-boto3-arc-zonal-shift (>=1.26.0,<1.27.0)", "mypy-boto3-athena (>=1.26.0,<1.27.0)", "mypy-boto3-auditmanager (>=1.26.0,<1.27.0)", "mypy-boto3-autoscaling (>=1.26.0,<1.27.0)", "mypy-boto3-autoscaling-plans (>=1.26.0,<1.27.0)", "mypy-boto3-backup (>=1.26.0,<1.27.0)", "mypy-boto3-backup-gateway (>=1.26.0,<1.27.0)", "mypy-boto3-backupstorage (>=1.26.0,<1.27.0)", "mypy-boto3-batch (>=1.26.0,<1.27.0)", "mypy-boto3-billingconductor (>=1.26.0,<1.27.0)", "mypy-boto3-braket (>=1.26.0,<1.27.0)", "mypy-boto3-budgets (>=1.26.0,<1.27.0)", "mypy-boto3-ce (>=1.26.0,<1.27.0)", "mypy-boto3-chime (>=1.26.0,<1.27.0)", "mypy-boto3-chime-sdk-identity (>=1.26.0,<1.27.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.26.0,<1.27.0)", "mypy-boto3-chime-sdk-meetings (>=1.26.0,<1.27.0)", "mypy-boto3-chime-sdk-messaging (>=1.26.0,<1.27.0)", "mypy-boto3-chime-sdk-voice (>=1.26.0,<1.27.0)", "mypy-boto3-cleanrooms (>=1.26.0,<1.27.0)", "mypy-boto3-cloud9 (>=1.26.0,<1.27.0)", "mypy-boto3-cloudcontrol (>=1.26.0,<1.27.0)", "mypy-boto3-clouddirectory (>=1.26.0,<1.27.0)", "mypy-boto3-cloudformation (>=1.26.0,<1.27.0)", "mypy-boto3-cloudfront (>=1.26.0,<1.27.0)", "mypy-boto3-cloudhsm (>=1.26.0,<1.27.0)", "mypy-boto3-cloudhsmv2 (>=1.26.0,<1.27.0)", "mypy-boto3-cloudsearch (>=1.26.0,<1.27.0)", "mypy-boto3-cloudsearchdomain (>=1.26.0,<1.27.0)", "mypy-boto3-cloudtrail (>=1.26.0,<1.27.0)", "mypy-boto3-cloudtrail-data (>=1.26.0,<1.27.0)", "mypy-boto3-cloudwatch (>=1.26.0,<1.27.0)", "mypy-boto3-codeartifact (>=1.26.0,<1.27.0)", "mypy-boto3-codebuild (>=1.26.0,<1.27.0)", "mypy-boto3-codecatalyst (>=1.26.0,<1.27.0)", "mypy-boto3-codecommit (>=1.26.0,<1.27.0)", "mypy-boto3-codedeploy (>=1.26.0,<1.27.0)", "mypy-boto3-codeguru-reviewer (>=1.26.0,<1.27.0)", "mypy-boto3-codeguruprofiler (>=1.26.0,<1.27.0)", "mypy-boto3-codepipeline (>=1.26.0,<1.27.0)", "mypy-boto3-codestar (>=1.26.0,<1.27.0)", "mypy-boto3-codestar-connections (>=1.26.0,<1.27.0)", "mypy-boto3-codestar-notifications (>=1.26.0,<1.27.0)", "mypy-boto3-cognito-identity (>=1.26.0,<1.27.0)", "mypy-boto3-cognito-idp (>=1.26.0,<1.27.0)", "mypy-boto3-cognito-sync (>=1.26.0,<1.27.0)", "mypy-boto3-comprehend (>=1.26.0,<1.27.0)", "mypy-boto3-comprehendmedical (>=1.26.0,<1.27.0)", "mypy-boto3-compute-optimizer (>=1.26.0,<1.27.0)", "mypy-boto3-config (>=1.26.0,<1.27.0)", "mypy-boto3-connect (>=1.26.0,<1.27.0)", "mypy-boto3-connect-contact-lens (>=1.26.0,<1.27.0)", "mypy-boto3-connectcampaigns (>=1.26.0,<1.27.0)", "mypy-boto3-connectcases (>=1.26.0,<1.27.0)", "mypy-boto3-connectparticipant (>=1.26.0,<1.27.0)", "mypy-boto3-controltower (>=1.26.0,<1.27.0)", "mypy-boto3-cur (>=1.26.0,<1.27.0)", "mypy-boto3-customer-profiles (>=1.26.0,<1.27.0)", "mypy-boto3-databrew (>=1.26.0,<1.27.0)", "mypy-boto3-dataexchange (>=1.26.0,<1.27.0)", "mypy-boto3-datapipeline (>=1.26.0,<1.27.0)", "mypy-boto3-datasync (>=1.26.0,<1.27.0)", "mypy-boto3-dax (>=1.26.0,<1.27.0)", "mypy-boto3-detective (>=1.26.0,<1.27.0)", "mypy-boto3-devicefarm (>=1.26.0,<1.27.0)", "mypy-boto3-devops-guru (>=1.26.0,<1.27.0)", "mypy-boto3-directconnect (>=1.26.0,<1.27.0)", "mypy-boto3-discovery (>=1.26.0,<1.27.0)", "mypy-boto3-dlm (>=1.26.0,<1.27.0)", "mypy-boto3-dms (>=1.26.0,<1.27.0)", "mypy-boto3-docdb (>=1.26.0,<1.27.0)", "mypy-boto3-docdb-elastic (>=1.26.0,<1.27.0)", "mypy-boto3-drs (>=1.26.0,<1.27.0)", "mypy-boto3-ds (>=1.26.0,<1.27.0)", "mypy-boto3-dynamodb (>=1.26.0,<1.27.0)", "mypy-boto3-dynamodbstreams (>=1.26.0,<1.27.0)", "mypy-boto3-ebs (>=1.26.0,<1.27.0)", "mypy-boto3-ec2 (>=1.26.0,<1.27.0)", "mypy-boto3-ec2-instance-connect (>=1.26.0,<1.27.0)", "mypy-boto3-ecr (>=1.26.0,<1.27.0)", "mypy-boto3-ecr-public (>=1.26.0,<1.27.0)", "mypy-boto3-ecs (>=1.26.0,<1.27.0)", "mypy-boto3-efs (>=1.26.0,<1.27.0)", "mypy-boto3-eks (>=1.26.0,<1.27.0)", "mypy-boto3-elastic-inference (>=1.26.0,<1.27.0)", "mypy-boto3-elasticache (>=1.26.0,<1.27.0)", "mypy-boto3-elasticbeanstalk (>=1.26.0,<1.27.0)", "mypy-boto3-elastictranscoder (>=1.26.0,<1.27.0)", "mypy-boto3-elb (>=1.26.0,<1.27.0)", "mypy-boto3-elbv2 (>=1.26.0,<1.27.0)", "mypy-boto3-emr (>=1.26.0,<1.27.0)", "mypy-boto3-emr-containers (>=1.26.0,<1.27.0)", "mypy-boto3-emr-serverless (>=1.26.0,<1.27.0)", "mypy-boto3-es (>=1.26.0,<1.27.0)", "mypy-boto3-events (>=1.26.0,<1.27.0)", "mypy-boto3-evidently (>=1.26.0,<1.27.0)", "mypy-boto3-finspace (>=1.26.0,<1.27.0)", "mypy-boto3-finspace-data (>=1.26.0,<1.27.0)", "mypy-boto3-firehose (>=1.26.0,<1.27.0)", "mypy-boto3-fis (>=1.26.0,<1.27.0)", "mypy-boto3-fms (>=1.26.0,<1.27.0)", "mypy-boto3-forecast (>=1.26.0,<1.27.0)", "mypy-boto3-forecastquery (>=1.26.0,<1.27.0)", "mypy-boto3-frauddetector (>=1.26.0,<1.27.0)", "mypy-boto3-fsx (>=1.26.0,<1.27.0)", "mypy-boto3-gamelift (>=1.26.0,<1.27.0)", "mypy-boto3-gamesparks (>=1.26.0,<1.27.0)", "mypy-boto3-glacier (>=1.26.0,<1.27.0)", "mypy-boto3-globalaccelerator (>=1.26.0,<1.27.0)", "mypy-boto3-glue (>=1.26.0,<1.27.0)", "mypy-boto3-grafana (>=1.26.0,<1.27.0)", "mypy-boto3-greengrass (>=1.26.0,<1.27.0)", "mypy-boto3-greengrassv2 (>=1.26.0,<1.27.0)", "mypy-boto3-groundstation (>=1.26.0,<1.27.0)", "mypy-boto3-guardduty (>=1.26.0,<1.27.0)", "mypy-boto3-health (>=1.26.0,<1.27.0)", "mypy-boto3-healthlake (>=1.26.0,<1.27.0)", "mypy-boto3-honeycode (>=1.26.0,<1.27.0)", "mypy-boto3-iam (>=1.26.0,<1.27.0)", "mypy-boto3-identitystore (>=1.26.0,<1.27.0)", "mypy-boto3-imagebuilder (>=1.26.0,<1.27.0)", "mypy-boto3-importexport (>=1.26.0,<1.27.0)", "mypy-boto3-inspector (>=1.26.0,<1.27.0)", "mypy-boto3-inspector2 (>=1.26.0,<1.27.0)", "mypy-boto3-iot (>=1.26.0,<1.27.0)", "mypy-boto3-iot-data (>=1.26.0,<1.27.0)", "mypy-boto3-iot-jobs-data (>=1.26.0,<1.27.0)", "mypy-boto3-iot-roborunner (>=1.26.0,<1.27.0)", "mypy-boto3-iot1click-devices (>=1.26.0,<1.27.0)", "mypy-boto3-iot1click-projects (>=1.26.0,<1.27.0)", "mypy-boto3-iotanalytics (>=1.26.0,<1.27.0)", "mypy-boto3-iotdeviceadvisor (>=1.26.0,<1.27.0)", "mypy-boto3-iotevents (>=1.26.0,<1.27.0)", "mypy-boto3-iotevents-data (>=1.26.0,<1.27.0)", "mypy-boto3-iotfleethub (>=1.26.0,<1.27.0)", "mypy-boto3-iotfleetwise (>=1.26.0,<1.27.0)", "mypy-boto3-iotsecuretunneling (>=1.26.0,<1.27.0)", "mypy-boto3-iotsitewise (>=1.26.0,<1.27.0)", "mypy-boto3-iotthingsgraph (>=1.26.0,<1.27.0)", "mypy-boto3-iottwinmaker (>=1.26.0,<1.27.0)", "mypy-boto3-iotwireless (>=1.26.0,<1.27.0)", "mypy-boto3-ivs (>=1.26.0,<1.27.0)", "mypy-boto3-ivschat (>=1.26.0,<1.27.0)", "mypy-boto3-kafka (>=1.26.0,<1.27.0)", "mypy-boto3-kafkaconnect (>=1.26.0,<1.27.0)", "mypy-boto3-kendra (>=1.26.0,<1.27.0)", "mypy-boto3-kendra-ranking (>=1.26.0,<1.27.0)", "mypy-boto3-keyspaces (>=1.26.0,<1.27.0)", "mypy-boto3-kinesis (>=1.26.0,<1.27.0)", "mypy-boto3-kinesis-video-archived-media (>=1.26.0,<1.27.0)", "mypy-boto3-kinesis-video-media (>=1.26.0,<1.27.0)", "mypy-boto3-kinesis-video-signaling (>=1.26.0,<1.27.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.26.0,<1.27.0)", "mypy-boto3-kinesisanalytics (>=1.26.0,<1.27.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.26.0,<1.27.0)", "mypy-boto3-kinesisvideo (>=1.26.0,<1.27.0)", "mypy-boto3-kms (>=1.26.0,<1.27.0)", "mypy-boto3-lakeformation (>=1.26.0,<1.27.0)", "mypy-boto3-lambda (>=1.26.0,<1.27.0)", "mypy-boto3-lex-models (>=1.26.0,<1.27.0)", "mypy-boto3-lex-runtime (>=1.26.0,<1.27.0)", "mypy-boto3-lexv2-models (>=1.26.0,<1.27.0)", "mypy-boto3-lexv2-runtime (>=1.26.0,<1.27.0)", "mypy-boto3-license-manager (>=1.26.0,<1.27.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.26.0,<1.27.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.26.0,<1.27.0)", "mypy-boto3-lightsail (>=1.26.0,<1.27.0)", "mypy-boto3-location (>=1.26.0,<1.27.0)", "mypy-boto3-logs (>=1.26.0,<1.27.0)", "mypy-boto3-lookoutequipment (>=1.26.0,<1.27.0)", "mypy-boto3-lookoutmetrics (>=1.26.0,<1.27.0)", "mypy-boto3-lookoutvision (>=1.26.0,<1.27.0)", "mypy-boto3-m2 (>=1.26.0,<1.27.0)", "mypy-boto3-machinelearning (>=1.26.0,<1.27.0)", "mypy-boto3-macie (>=1.26.0,<1.27.0)", "mypy-boto3-macie2 (>=1.26.0,<1.27.0)", "mypy-boto3-managedblockchain (>=1.26.0,<1.27.0)", "mypy-boto3-marketplace-catalog (>=1.26.0,<1.27.0)", "mypy-boto3-marketplace-entitlement (>=1.26.0,<1.27.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.26.0,<1.27.0)", "mypy-boto3-mediaconnect (>=1.26.0,<1.27.0)", "mypy-boto3-mediaconvert (>=1.26.0,<1.27.0)", "mypy-boto3-medialive (>=1.26.0,<1.27.0)", "mypy-boto3-mediapackage (>=1.26.0,<1.27.0)", "mypy-boto3-mediapackage-vod (>=1.26.0,<1.27.0)", "mypy-boto3-mediastore (>=1.26.0,<1.27.0)", "mypy-boto3-mediastore-data (>=1.26.0,<1.27.0)", "mypy-boto3-mediatailor (>=1.26.0,<1.27.0)", "mypy-boto3-memorydb (>=1.26.0,<1.27.0)", "mypy-boto3-meteringmarketplace (>=1.26.0,<1.27.0)", "mypy-boto3-mgh (>=1.26.0,<1.27.0)", "mypy-boto3-mgn (>=1.26.0,<1.27.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.26.0,<1.27.0)", "mypy-boto3-migrationhub-config (>=1.26.0,<1.27.0)", "mypy-boto3-migrationhuborchestrator (>=1.26.0,<1.27.0)", "mypy-boto3-migrationhubstrategy (>=1.26.0,<1.27.0)", "mypy-boto3-mobile (>=1.26.0,<1.27.0)", "mypy-boto3-mq (>=1.26.0,<1.27.0)", "mypy-boto3-mturk (>=1.26.0,<1.27.0)", "mypy-boto3-mwaa (>=1.26.0,<1.27.0)", "mypy-boto3-neptune (>=1.26.0,<1.27.0)", "mypy-boto3-network-firewall (>=1.26.0,<1.27.0)", "mypy-boto3-networkmanager (>=1.26.0,<1.27.0)", "mypy-boto3-nimble (>=1.26.0,<1.27.0)", "mypy-boto3-oam (>=1.26.0,<1.27.0)", "mypy-boto3-omics (>=1.26.0,<1.27.0)", "mypy-boto3-opensearch (>=1.26.0,<1.27.0)", "mypy-boto3-opensearchserverless (>=1.26.0,<1.27.0)", "mypy-boto3-opsworks (>=1.26.0,<1.27.0)", "mypy-boto3-opsworkscm (>=1.26.0,<1.27.0)", "mypy-boto3-organizations (>=1.26.0,<1.27.0)", "mypy-boto3-outposts (>=1.26.0,<1.27.0)", "mypy-boto3-panorama (>=1.26.0,<1.27.0)", "mypy-boto3-personalize (>=1.26.0,<1.27.0)", "mypy-boto3-personalize-events (>=1.26.0,<1.27.0)", "mypy-boto3-personalize-runtime (>=1.26.0,<1.27.0)", "mypy-boto3-pi (>=1.26.0,<1.27.0)", "mypy-boto3-pinpoint (>=1.26.0,<1.27.0)", "mypy-boto3-pinpoint-email (>=1.26.0,<1.27.0)", "mypy-boto3-pinpoint-sms-voice (>=1.26.0,<1.27.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.26.0,<1.27.0)", "mypy-boto3-pipes (>=1.26.0,<1.27.0)", "mypy-boto3-polly (>=1.26.0,<1.27.0)", "mypy-boto3-pricing (>=1.26.0,<1.27.0)", "mypy-boto3-privatenetworks (>=1.26.0,<1.27.0)", "mypy-boto3-proton (>=1.26.0,<1.27.0)", "mypy-boto3-qldb (>=1.26.0,<1.27.0)", "mypy-boto3-qldb-session (>=1.26.0,<1.27.0)", "mypy-boto3-quicksight (>=1.26.0,<1.27.0)", "mypy-boto3-ram (>=1.26.0,<1.27.0)", "mypy-boto3-rbin (>=1.26.0,<1.27.0)", "mypy-boto3-rds (>=1.26.0,<1.27.0)", "mypy-boto3-rds-data (>=1.26.0,<1.27.0)", "mypy-boto3-redshift (>=1.26.0,<1.27.0)", "mypy-boto3-redshift-data (>=1.26.0,<1.27.0)", "mypy-boto3-redshift-serverless (>=1.26.0,<1.27.0)", "mypy-boto3-rekognition (>=1.26.0,<1.27.0)", "mypy-boto3-resiliencehub (>=1.26.0,<1.27.0)", "mypy-boto3-resource-explorer-2 (>=1.26.0,<1.27.0)", "mypy-boto3-resource-groups (>=1.26.0,<1.27.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.26.0,<1.27.0)", "mypy-boto3-robomaker (>=1.26.0,<1.27.0)", "mypy-boto3-rolesanywhere (>=1.26.0,<1.27.0)", "mypy-boto3-route53 (>=1.26.0,<1.27.0)", "mypy-boto3-route53-recovery-cluster (>=1.26.0,<1.27.0)", "mypy-boto3-route53-recovery-control-config (>=1.26.0,<1.27.0)", "mypy-boto3-route53-recovery-readiness (>=1.26.0,<1.27.0)", "mypy-boto3-route53domains (>=1.26.0,<1.27.0)", "mypy-boto3-route53resolver (>=1.26.0,<1.27.0)", "mypy-boto3-rum (>=1.26.0,<1.27.0)", "mypy-boto3-s3 (>=1.26.0,<1.27.0)", "mypy-boto3-s3control (>=1.26.0,<1.27.0)", "mypy-boto3-s3outposts (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker-edge (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker-geospatial (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker-metrics (>=1.26.0,<1.27.0)", "mypy-boto3-sagemaker-runtime (>=1.26.0,<1.27.0)", "mypy-boto3-savingsplans (>=1.26.0,<1.27.0)", "mypy-boto3-scheduler (>=1.26.0,<1.27.0)", "mypy-boto3-schemas (>=1.26.0,<1.27.0)", "mypy-boto3-sdb (>=1.26.0,<1.27.0)", "mypy-boto3-secretsmanager (>=1.26.0,<1.27.0)", "mypy-boto3-securityhub (>=1.26.0,<1.27.0)", "mypy-boto3-securitylake (>=1.26.0,<1.27.0)", "mypy-boto3-serverlessrepo (>=1.26.0,<1.27.0)", "mypy-boto3-service-quotas (>=1.26.0,<1.27.0)", "mypy-boto3-servicecatalog (>=1.26.0,<1.27.0)", "mypy-boto3-servicecatalog-appregistry (>=1.26.0,<1.27.0)", "mypy-boto3-servicediscovery (>=1.26.0,<1.27.0)", "mypy-boto3-ses (>=1.26.0,<1.27.0)", "mypy-boto3-sesv2 (>=1.26.0,<1.27.0)", "mypy-boto3-shield (>=1.26.0,<1.27.0)", "mypy-boto3-signer (>=1.26.0,<1.27.0)", "mypy-boto3-simspaceweaver (>=1.26.0,<1.27.0)", "mypy-boto3-sms (>=1.26.0,<1.27.0)", "mypy-boto3-sms-voice (>=1.26.0,<1.27.0)", "mypy-boto3-snow-device-management (>=1.26.0,<1.27.0)", "mypy-boto3-snowball (>=1.26.0,<1.27.0)", "mypy-boto3-sns (>=1.26.0,<1.27.0)", "mypy-boto3-sqs (>=1.26.0,<1.27.0)", "mypy-boto3-ssm (>=1.26.0,<1.27.0)", "mypy-boto3-ssm-contacts (>=1.26.0,<1.27.0)", "mypy-boto3-ssm-incidents (>=1.26.0,<1.27.0)", "mypy-boto3-ssm-sap (>=1.26.0,<1.27.0)", "mypy-boto3-sso (>=1.26.0,<1.27.0)", "mypy-boto3-sso-admin (>=1.26.0,<1.27.0)", "mypy-boto3-sso-oidc (>=1.26.0,<1.27.0)", "mypy-boto3-stepfunctions (>=1.26.0,<1.27.0)", "mypy-boto3-storagegateway (>=1.26.0,<1.27.0)", "mypy-boto3-sts (>=1.26.0,<1.27.0)", "mypy-boto3-support (>=1.26.0,<1.27.0)", "mypy-boto3-support-app (>=1.26.0,<1.27.0)", "mypy-boto3-swf (>=1.26.0,<1.27.0)", "mypy-boto3-synthetics (>=1.26.0,<1.27.0)", "mypy-boto3-textract (>=1.26.0,<1.27.0)", "mypy-boto3-timestream-query (>=1.26.0,<1.27.0)", "mypy-boto3-timestream-write (>=1.26.0,<1.27.0)", "mypy-boto3-transcribe (>=1.26.0,<1.27.0)", "mypy-boto3-transfer (>=1.26.0,<1.27.0)", "mypy-boto3-translate (>=1.26.0,<1.27.0)", "mypy-boto3-voice-id (>=1.26.0,<1.27.0)", "mypy-boto3-waf (>=1.26.0,<1.27.0)", "mypy-boto3-waf-regional (>=1.26.0,<1.27.0)", "mypy-boto3-wafv2 (>=1.26.0,<1.27.0)", "mypy-boto3-wellarchitected (>=1.26.0,<1.27.0)", "mypy-boto3-wisdom (>=1.26.0,<1.27.0)", "mypy-boto3-workdocs (>=1.26.0,<1.27.0)", "mypy-boto3-worklink (>=1.26.0,<1.27.0)", "mypy-boto3-workmail (>=1.26.0,<1.27.0)", "mypy-boto3-workmailmessageflow (>=1.26.0,<1.27.0)", "mypy-boto3-workspaces (>=1.26.0,<1.27.0)", "mypy-boto3-workspaces-web (>=1.26.0,<1.27.0)", "mypy-boto3-xray (>=1.26.0,<1.27.0)"] +amp = ["mypy-boto3-amp (>=1.26.0,<1.27.0)"] +amplify = ["mypy-boto3-amplify (>=1.26.0,<1.27.0)"] +amplifybackend = ["mypy-boto3-amplifybackend (>=1.26.0,<1.27.0)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.26.0,<1.27.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.26.0,<1.27.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.26.0,<1.27.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.26.0,<1.27.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.26.0,<1.27.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.26.0,<1.27.0)"] +appflow = ["mypy-boto3-appflow (>=1.26.0,<1.27.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.26.0,<1.27.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.26.0,<1.27.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.26.0,<1.27.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.26.0,<1.27.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.26.0,<1.27.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.26.0,<1.27.0)"] +appstream = ["mypy-boto3-appstream (>=1.26.0,<1.27.0)"] +appsync = ["mypy-boto3-appsync (>=1.26.0,<1.27.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.26.0,<1.27.0)"] +athena = ["mypy-boto3-athena (>=1.26.0,<1.27.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.26.0,<1.27.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.26.0,<1.27.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.26.0,<1.27.0)"] +backup = ["mypy-boto3-backup (>=1.26.0,<1.27.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.26.0,<1.27.0)"] +backupstorage = ["mypy-boto3-backupstorage (>=1.26.0,<1.27.0)"] +batch = ["mypy-boto3-batch (>=1.26.0,<1.27.0)"] +billingconductor = ["mypy-boto3-billingconductor (>=1.26.0,<1.27.0)"] +boto3 = ["boto3 (==1.26.69)", "botocore (==1.29.69)"] +braket = ["mypy-boto3-braket (>=1.26.0,<1.27.0)"] +budgets = ["mypy-boto3-budgets (>=1.26.0,<1.27.0)"] +ce = ["mypy-boto3-ce (>=1.26.0,<1.27.0)"] +chime = ["mypy-boto3-chime (>=1.26.0,<1.27.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.26.0,<1.27.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.26.0,<1.27.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.26.0,<1.27.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.26.0,<1.27.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.26.0,<1.27.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.26.0,<1.27.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.26.0,<1.27.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.26.0,<1.27.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.26.0,<1.27.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.26.0,<1.27.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.26.0,<1.27.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.26.0,<1.27.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.26.0,<1.27.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.26.0,<1.27.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.26.0,<1.27.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.26.0,<1.27.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.26.0,<1.27.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.26.0,<1.27.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.26.0,<1.27.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.26.0,<1.27.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.26.0,<1.27.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.26.0,<1.27.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.26.0,<1.27.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.26.0,<1.27.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.26.0,<1.27.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.26.0,<1.27.0)"] +codestar = ["mypy-boto3-codestar (>=1.26.0,<1.27.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.26.0,<1.27.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.26.0,<1.27.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.26.0,<1.27.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.26.0,<1.27.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.26.0,<1.27.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.26.0,<1.27.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.26.0,<1.27.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.26.0,<1.27.0)"] +config = ["mypy-boto3-config (>=1.26.0,<1.27.0)"] +connect = ["mypy-boto3-connect (>=1.26.0,<1.27.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.26.0,<1.27.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.26.0,<1.27.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.26.0,<1.27.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.26.0,<1.27.0)"] +controltower = ["mypy-boto3-controltower (>=1.26.0,<1.27.0)"] +cur = ["mypy-boto3-cur (>=1.26.0,<1.27.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.26.0,<1.27.0)"] +databrew = ["mypy-boto3-databrew (>=1.26.0,<1.27.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.26.0,<1.27.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.26.0,<1.27.0)"] +datasync = ["mypy-boto3-datasync (>=1.26.0,<1.27.0)"] +dax = ["mypy-boto3-dax (>=1.26.0,<1.27.0)"] +detective = ["mypy-boto3-detective (>=1.26.0,<1.27.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.26.0,<1.27.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.26.0,<1.27.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.26.0,<1.27.0)"] +discovery = ["mypy-boto3-discovery (>=1.26.0,<1.27.0)"] +dlm = ["mypy-boto3-dlm (>=1.26.0,<1.27.0)"] +dms = ["mypy-boto3-dms (>=1.26.0,<1.27.0)"] +docdb = ["mypy-boto3-docdb (>=1.26.0,<1.27.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.26.0,<1.27.0)"] +drs = ["mypy-boto3-drs (>=1.26.0,<1.27.0)"] +ds = ["mypy-boto3-ds (>=1.26.0,<1.27.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.26.0,<1.27.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.26.0,<1.27.0)"] +ebs = ["mypy-boto3-ebs (>=1.26.0,<1.27.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.26.0,<1.27.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.26.0,<1.27.0)"] +ecr = ["mypy-boto3-ecr (>=1.26.0,<1.27.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.26.0,<1.27.0)"] +ecs = ["mypy-boto3-ecs (>=1.26.0,<1.27.0)"] +efs = ["mypy-boto3-efs (>=1.26.0,<1.27.0)"] +eks = ["mypy-boto3-eks (>=1.26.0,<1.27.0)"] +elastic-inference = ["mypy-boto3-elastic-inference (>=1.26.0,<1.27.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.26.0,<1.27.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.26.0,<1.27.0)"] +elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.26.0,<1.27.0)"] +elb = ["mypy-boto3-elb (>=1.26.0,<1.27.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.26.0,<1.27.0)"] +emr = ["mypy-boto3-emr (>=1.26.0,<1.27.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.26.0,<1.27.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.26.0,<1.27.0)"] +es = ["mypy-boto3-es (>=1.26.0,<1.27.0)"] +essential = ["mypy-boto3-cloudformation (>=1.26.0,<1.27.0)", "mypy-boto3-dynamodb (>=1.26.0,<1.27.0)", "mypy-boto3-ec2 (>=1.26.0,<1.27.0)", "mypy-boto3-lambda (>=1.26.0,<1.27.0)", "mypy-boto3-rds (>=1.26.0,<1.27.0)", "mypy-boto3-s3 (>=1.26.0,<1.27.0)", "mypy-boto3-sqs (>=1.26.0,<1.27.0)"] +events = ["mypy-boto3-events (>=1.26.0,<1.27.0)"] +evidently = ["mypy-boto3-evidently (>=1.26.0,<1.27.0)"] +finspace = ["mypy-boto3-finspace (>=1.26.0,<1.27.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.26.0,<1.27.0)"] +firehose = ["mypy-boto3-firehose (>=1.26.0,<1.27.0)"] +fis = ["mypy-boto3-fis (>=1.26.0,<1.27.0)"] +fms = ["mypy-boto3-fms (>=1.26.0,<1.27.0)"] +forecast = ["mypy-boto3-forecast (>=1.26.0,<1.27.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.26.0,<1.27.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.26.0,<1.27.0)"] +fsx = ["mypy-boto3-fsx (>=1.26.0,<1.27.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.26.0,<1.27.0)"] +gamesparks = ["mypy-boto3-gamesparks (>=1.26.0,<1.27.0)"] +glacier = ["mypy-boto3-glacier (>=1.26.0,<1.27.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.26.0,<1.27.0)"] +glue = ["mypy-boto3-glue (>=1.26.0,<1.27.0)"] +grafana = ["mypy-boto3-grafana (>=1.26.0,<1.27.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.26.0,<1.27.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.26.0,<1.27.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.26.0,<1.27.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.26.0,<1.27.0)"] +health = ["mypy-boto3-health (>=1.26.0,<1.27.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.26.0,<1.27.0)"] +honeycode = ["mypy-boto3-honeycode (>=1.26.0,<1.27.0)"] +iam = ["mypy-boto3-iam (>=1.26.0,<1.27.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.26.0,<1.27.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.26.0,<1.27.0)"] +importexport = ["mypy-boto3-importexport (>=1.26.0,<1.27.0)"] +inspector = ["mypy-boto3-inspector (>=1.26.0,<1.27.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.26.0,<1.27.0)"] +iot = ["mypy-boto3-iot (>=1.26.0,<1.27.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.26.0,<1.27.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.26.0,<1.27.0)"] +iot-roborunner = ["mypy-boto3-iot-roborunner (>=1.26.0,<1.27.0)"] +iot1click-devices = ["mypy-boto3-iot1click-devices (>=1.26.0,<1.27.0)"] +iot1click-projects = ["mypy-boto3-iot1click-projects (>=1.26.0,<1.27.0)"] +iotanalytics = ["mypy-boto3-iotanalytics (>=1.26.0,<1.27.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.26.0,<1.27.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.26.0,<1.27.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.26.0,<1.27.0)"] +iotfleethub = ["mypy-boto3-iotfleethub (>=1.26.0,<1.27.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.26.0,<1.27.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.26.0,<1.27.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.26.0,<1.27.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.26.0,<1.27.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.26.0,<1.27.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.26.0,<1.27.0)"] +ivs = ["mypy-boto3-ivs (>=1.26.0,<1.27.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.26.0,<1.27.0)"] +kafka = ["mypy-boto3-kafka (>=1.26.0,<1.27.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.26.0,<1.27.0)"] +kendra = ["mypy-boto3-kendra (>=1.26.0,<1.27.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.26.0,<1.27.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.26.0,<1.27.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.26.0,<1.27.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.26.0,<1.27.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.26.0,<1.27.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.26.0,<1.27.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.26.0,<1.27.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.26.0,<1.27.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.26.0,<1.27.0)"] +kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.26.0,<1.27.0)"] +kms = ["mypy-boto3-kms (>=1.26.0,<1.27.0)"] +lakeformation = ["mypy-boto3-lakeformation (>=1.26.0,<1.27.0)"] +lambda = ["mypy-boto3-lambda (>=1.26.0,<1.27.0)"] +lex-models = ["mypy-boto3-lex-models (>=1.26.0,<1.27.0)"] +lex-runtime = ["mypy-boto3-lex-runtime (>=1.26.0,<1.27.0)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.26.0,<1.27.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.26.0,<1.27.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.26.0,<1.27.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.26.0,<1.27.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.26.0,<1.27.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.26.0,<1.27.0)"] +location = ["mypy-boto3-location (>=1.26.0,<1.27.0)"] +logs = ["mypy-boto3-logs (>=1.26.0,<1.27.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.26.0,<1.27.0)"] +lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.26.0,<1.27.0)"] +lookoutvision = ["mypy-boto3-lookoutvision (>=1.26.0,<1.27.0)"] +m2 = ["mypy-boto3-m2 (>=1.26.0,<1.27.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.26.0,<1.27.0)"] +macie = ["mypy-boto3-macie (>=1.26.0,<1.27.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.26.0,<1.27.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.26.0,<1.27.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.26.0,<1.27.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.26.0,<1.27.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.26.0,<1.27.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.26.0,<1.27.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.26.0,<1.27.0)"] +medialive = ["mypy-boto3-medialive (>=1.26.0,<1.27.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.26.0,<1.27.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.26.0,<1.27.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.26.0,<1.27.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.26.0,<1.27.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.26.0,<1.27.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.26.0,<1.27.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.26.0,<1.27.0)"] +mgh = ["mypy-boto3-mgh (>=1.26.0,<1.27.0)"] +mgn = ["mypy-boto3-mgn (>=1.26.0,<1.27.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.26.0,<1.27.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.26.0,<1.27.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.26.0,<1.27.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.26.0,<1.27.0)"] +mobile = ["mypy-boto3-mobile (>=1.26.0,<1.27.0)"] +mq = ["mypy-boto3-mq (>=1.26.0,<1.27.0)"] +mturk = ["mypy-boto3-mturk (>=1.26.0,<1.27.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.26.0,<1.27.0)"] +neptune = ["mypy-boto3-neptune (>=1.26.0,<1.27.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.26.0,<1.27.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.26.0,<1.27.0)"] +nimble = ["mypy-boto3-nimble (>=1.26.0,<1.27.0)"] +oam = ["mypy-boto3-oam (>=1.26.0,<1.27.0)"] +omics = ["mypy-boto3-omics (>=1.26.0,<1.27.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.26.0,<1.27.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.26.0,<1.27.0)"] +opsworks = ["mypy-boto3-opsworks (>=1.26.0,<1.27.0)"] +opsworkscm = ["mypy-boto3-opsworkscm (>=1.26.0,<1.27.0)"] +organizations = ["mypy-boto3-organizations (>=1.26.0,<1.27.0)"] +outposts = ["mypy-boto3-outposts (>=1.26.0,<1.27.0)"] +panorama = ["mypy-boto3-panorama (>=1.26.0,<1.27.0)"] +personalize = ["mypy-boto3-personalize (>=1.26.0,<1.27.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.26.0,<1.27.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.26.0,<1.27.0)"] +pi = ["mypy-boto3-pi (>=1.26.0,<1.27.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.26.0,<1.27.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.26.0,<1.27.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.26.0,<1.27.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.26.0,<1.27.0)"] +pipes = ["mypy-boto3-pipes (>=1.26.0,<1.27.0)"] +polly = ["mypy-boto3-polly (>=1.26.0,<1.27.0)"] +pricing = ["mypy-boto3-pricing (>=1.26.0,<1.27.0)"] +privatenetworks = ["mypy-boto3-privatenetworks (>=1.26.0,<1.27.0)"] +proton = ["mypy-boto3-proton (>=1.26.0,<1.27.0)"] +qldb = ["mypy-boto3-qldb (>=1.26.0,<1.27.0)"] +qldb-session = ["mypy-boto3-qldb-session (>=1.26.0,<1.27.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.26.0,<1.27.0)"] +ram = ["mypy-boto3-ram (>=1.26.0,<1.27.0)"] +rbin = ["mypy-boto3-rbin (>=1.26.0,<1.27.0)"] +rds = ["mypy-boto3-rds (>=1.26.0,<1.27.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.26.0,<1.27.0)"] +redshift = ["mypy-boto3-redshift (>=1.26.0,<1.27.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.26.0,<1.27.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.26.0,<1.27.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.26.0,<1.27.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.26.0,<1.27.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.26.0,<1.27.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.26.0,<1.27.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.26.0,<1.27.0)"] +robomaker = ["mypy-boto3-robomaker (>=1.26.0,<1.27.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.26.0,<1.27.0)"] +route53 = ["mypy-boto3-route53 (>=1.26.0,<1.27.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.26.0,<1.27.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.26.0,<1.27.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.26.0,<1.27.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.26.0,<1.27.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.26.0,<1.27.0)"] +rum = ["mypy-boto3-rum (>=1.26.0,<1.27.0)"] +s3 = ["mypy-boto3-s3 (>=1.26.0,<1.27.0)"] +s3control = ["mypy-boto3-s3control (>=1.26.0,<1.27.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.26.0,<1.27.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.26.0,<1.27.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.26.0,<1.27.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.26.0,<1.27.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.26.0,<1.27.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.26.0,<1.27.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.26.0,<1.27.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.26.0,<1.27.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.26.0,<1.27.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.26.0,<1.27.0)"] +schemas = ["mypy-boto3-schemas (>=1.26.0,<1.27.0)"] +sdb = ["mypy-boto3-sdb (>=1.26.0,<1.27.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.26.0,<1.27.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.26.0,<1.27.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.26.0,<1.27.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.26.0,<1.27.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.26.0,<1.27.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.26.0,<1.27.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.26.0,<1.27.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.26.0,<1.27.0)"] +ses = ["mypy-boto3-ses (>=1.26.0,<1.27.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.26.0,<1.27.0)"] +shield = ["mypy-boto3-shield (>=1.26.0,<1.27.0)"] +signer = ["mypy-boto3-signer (>=1.26.0,<1.27.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.26.0,<1.27.0)"] +sms = ["mypy-boto3-sms (>=1.26.0,<1.27.0)"] +sms-voice = ["mypy-boto3-sms-voice (>=1.26.0,<1.27.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.26.0,<1.27.0)"] +snowball = ["mypy-boto3-snowball (>=1.26.0,<1.27.0)"] +sns = ["mypy-boto3-sns (>=1.26.0,<1.27.0)"] +sqs = ["mypy-boto3-sqs (>=1.26.0,<1.27.0)"] +ssm = ["mypy-boto3-ssm (>=1.26.0,<1.27.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.26.0,<1.27.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.26.0,<1.27.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.26.0,<1.27.0)"] +sso = ["mypy-boto3-sso (>=1.26.0,<1.27.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.26.0,<1.27.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.26.0,<1.27.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.26.0,<1.27.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.26.0,<1.27.0)"] +sts = ["mypy-boto3-sts (>=1.26.0,<1.27.0)"] +support = ["mypy-boto3-support (>=1.26.0,<1.27.0)"] +support-app = ["mypy-boto3-support-app (>=1.26.0,<1.27.0)"] +swf = ["mypy-boto3-swf (>=1.26.0,<1.27.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.26.0,<1.27.0)"] +textract = ["mypy-boto3-textract (>=1.26.0,<1.27.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.26.0,<1.27.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.26.0,<1.27.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.26.0,<1.27.0)"] +transfer = ["mypy-boto3-transfer (>=1.26.0,<1.27.0)"] +translate = ["mypy-boto3-translate (>=1.26.0,<1.27.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.26.0,<1.27.0)"] +waf = ["mypy-boto3-waf (>=1.26.0,<1.27.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.26.0,<1.27.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.26.0,<1.27.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.26.0,<1.27.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.26.0,<1.27.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.26.0,<1.27.0)"] +worklink = ["mypy-boto3-worklink (>=1.26.0,<1.27.0)"] +workmail = ["mypy-boto3-workmail (>=1.26.0,<1.27.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.26.0,<1.27.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.26.0,<1.27.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.26.0,<1.27.0)"] +xray = ["mypy-boto3-xray (>=1.26.0,<1.27.0)"] + [[package]] name = "botocore" version = "1.28.5" @@ -70,6 +428,17 @@ urllib3 = ">=1.25.4,<1.27" [package.extras] crt = ["awscrt (==0.14.0)"] +[[package]] +name = "botocore-stubs" +version = "1.29.69" +description = "Type annotations and code completion for botocore" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +types-awscrt = "*" + [[package]] name = "certifi" version = "2022.12.7" @@ -78,6 +447,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "2.1.1" @@ -116,6 +496,27 @@ python-versions = ">=3.7" [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "39.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "ruff", "mypy", "types-pytz", "types-requests", "check-manifest"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-shard (>=0.1.2)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] + [[package]] name = "dill" version = "0.3.6" @@ -175,6 +576,20 @@ docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo" perf = ["ipython"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "jmespath" version = "1.0.1" @@ -183,6 +598,14 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" + [[package]] name = "mccabe" version = "0.7.0" @@ -199,6 +622,49 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "moto" +version = "4.1.2" +description = "" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +boto3 = ">=1.9.201" +botocore = ">=1.12.201" +cryptography = ">=3.3.1" +Jinja2 = ">=2.10.1" +python-dateutil = ">=2.1,<3.0.0" +requests = ">=2.5" +responses = ">=0.13.0" +werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" +xmltodict = "*" + +[package.extras] +all = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (!=0.15)", "docker (>=2.5.1)", "graphql-core", "PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "sshpubkeys (>=3.1.0)", "openapi-spec-validator (>=0.2.8)", "pyparsing (>=3.0.7)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] +apigateway = ["PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (!=0.15)", "openapi-spec-validator (>=0.2.8)"] +apigatewayv2 = ["PyYAML (>=5.1)"] +appsync = ["graphql-core"] +awslambda = ["docker (>=2.5.1)"] +batch = ["docker (>=2.5.1)"] +cloudformation = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (!=0.15)", "docker (>=2.5.1)", "graphql-core", "PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "sshpubkeys (>=3.1.0)", "openapi-spec-validator (>=0.2.8)", "pyparsing (>=3.0.7)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] +cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (!=0.15)"] +ds = ["sshpubkeys (>=3.1.0)"] +dynamodb = ["docker (>=2.5.1)"] +dynamodbstreams = ["docker (>=2.5.1)"] +ebs = ["sshpubkeys (>=3.1.0)"] +ec2 = ["sshpubkeys (>=3.1.0)"] +efs = ["sshpubkeys (>=3.1.0)"] +eks = ["sshpubkeys (>=3.1.0)"] +glue = ["pyparsing (>=3.0.7)"] +iotdata = ["jsondiff (>=1.1.2)"] +route53resolver = ["sshpubkeys (>=3.1.0)"] +s3 = ["PyYAML (>=5.1)"] +server = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (!=0.15)", "docker (>=2.5.1)", "graphql-core", "PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "sshpubkeys (>=3.1.0)", "openapi-spec-validator (>=0.2.8)", "pyparsing (>=3.0.7)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "setuptools", "flask (!=2.2.0,!=2.2.1)", "flask-cors"] +ssm = ["PyYAML (>=5.1)"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] + [[package]] name = "multiprocess" version = "0.70.14" @@ -229,6 +695,17 @@ install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] +[[package]] +name = "mypy-boto3-stepfunctions" +version = "1.26.21" +description = "Type annotations for boto3.SFN 1.26.21 service generated with mypy-boto3-builder 7.11.11" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.1.0" + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -352,6 +829,29 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.10.4" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pyflakes" version = "3.0.1" @@ -382,6 +882,20 @@ wcwidth = "*" checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.10.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -427,6 +941,23 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "responses" +version = "0.22.0" +description = "A utility library for mocking out the `requests` Python library." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +requests = ">=2.22.0,<3.0" +toml = "*" +types-toml = "*" +urllib3 = ">=1.25.10" + +[package.extras] +tests = ["pytest (>=7.0.0)", "coverage (>=6.0.0)", "pytest-cov", "pytest-asyncio", "pytest-httpserver", "flake8", "types-requests", "mypy"] + [[package]] name = "s3transfer" version = "0.6.0" @@ -512,6 +1043,14 @@ sagemaker = ">=2.1.0" [package.extras] test = ["tox (>=3.13.1)", "pytest (>=4.4.1)", "stopit (==1.1.2)", "tensorflow (>=1.3.0)", "mock (>=2.0.0)", "contextlib2 (==0.5.5)", "ipython"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "tomli" version = "2.0.1" @@ -520,6 +1059,33 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "types-awscrt" +version = "0.16.10" +description = "Type annotations and code completion for awscrt" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[[package]] +name = "types-s3transfer" +version = "0.6.0.post5" +description = "Type annotations and code completion for s3transfer" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +types-awscrt = "*" + +[[package]] +name = "types-toml" +version = "0.10.8.3" +description = "Typing stubs for toml" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "4.4.0" @@ -549,6 +1115,28 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "werkzeug" +version = "2.2.2" +description = "The comprehensive WSGI web application library." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +category = "dev" +optional = false +python-versions = ">=3.4" + [[package]] name = "zipp" version = "3.10.0" @@ -564,19 +1152,23 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "78ff095f2c9a60b2027748e7f4d66a396491870bc7486ada9c7aee43a2f6adce" +content-hash = "83b0586fb3caad755a3438622280ae8531c52bc0f6c226da06907afd9090924f" [metadata.files] atomicwrites = [] attrs = [] aws-lambda-powertools = [] boto3 = [] +boto3-stubs = [] botocore = [] +botocore-stubs = [] certifi = [] +cffi = [] charset-normalizer = [] colorama = [] contextlib2 = [] coverage = [] +cryptography = [] dill = [] flake8 = [] google-pasta = [ @@ -586,14 +1178,18 @@ google-pasta = [ ] idna = [] importlib-metadata = [] +jinja2 = [] jmespath = [] +markupsafe = [] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] more-itertools = [] +moto = [] multiprocess = [] mypy = [] +mypy-boto3-stepfunctions = [] mypy-extensions = [] numpy = [] packaging = [] @@ -614,11 +1210,17 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydantic = [] pyflakes = [] pytest = [ {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] +pytest-mock = [] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -660,6 +1262,7 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] requests = [] +responses = [] s3transfer = [] sagemaker = [] schema = [] @@ -674,14 +1277,20 @@ smdebug-rulesconfig = [ stepfunctions = [ {file = "stepfunctions-2.3.0.tar.gz", hash = "sha256:77c668952e2532762e5beb8afe53ab684b9d2964418c4a4b5730c8e40100c3e9"}, ] +toml = [] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +types-awscrt = [] +types-s3transfer = [] +types-toml = [] typing-extensions = [] urllib3 = [] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] +werkzeug = [] +xmltodict = [] zipp = [] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 406729a7..5d988e89 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -10,6 +10,8 @@ requests = "^2.28.1" stepfunctions = "^2.3.0" protobuf = "3.20.*" aws-lambda-powertools = "^2.7.1" +pydantic = "^1.10.4" +boto3-stubs = {extras = ["stepfunctions"], version = "^1.26.69"} [tool.poetry.dev-dependencies] pytest = "^5.2" @@ -17,6 +19,8 @@ boto3 = "^1.24.45" flake8 = "^6.0.0" coverage = "^7.1.0" mypy = "^1.0.0" +pytest-mock = "^3.10.0" +moto = "^4.1.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/backend/requirements.txt b/backend/requirements.txt index 9bdcdaaa..d2e8ddfa 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,31 +1,40 @@ -attrs==22.1.0; python_version >= "3.5" -boto3==1.24.45; python_version >= "3.7" -botocore==1.27.45; python_version >= "3.7" -certifi==2022.6.15; python_version >= "3.7" and python_version < "4" -charset-normalizer==2.1.0; python_version >= "3.7" and python_version < "4" and python_full_version >= "3.6.0" -dill==0.3.5.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" -google-pasta==0.2.0 -idna==3.3; python_version >= "3.7" and python_version < "4" -importlib-metadata==4.12.0; python_version >= "3.7" +attrs==22.1.0; python_version >= "3.6" +aws-lambda-powertools==2.7.1; python_full_version >= "3.7.4" and python_full_version < "4.0.0" +boto3-stubs==1.26.69; python_version >= "3.7" +boto3==1.25.5; python_version >= "3.7" +botocore-stubs==1.29.69; python_version >= "3.7" and python_version < "4.0" +botocore==1.28.5; python_version >= "3.7" +certifi==2022.12.7; python_version >= "3.7" and python_version < "4" +charset-normalizer==2.1.1; python_version >= "3.7" and python_version < "4" and python_full_version >= "3.6.0" +contextlib2==21.6.0; python_version >= "3.6" +dill==0.3.6; python_version >= "3.7" +google-pasta==0.2.0; python_version >= "3.6" +idna==3.4; python_version >= "3.7" and python_version < "4" +importlib-metadata==4.13.0; python_version >= "3.7" jmespath==1.0.1; python_version >= "3.7" -multiprocess==0.70.13; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" -numpy==1.23.1 -packaging==21.3; python_version >= "3.6" -pandas==1.4.3; python_version >= "3.8" -pathos==0.2.9; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" -pox==0.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" -ppft==1.7.6.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" -protobuf3-to-dict==0.1.5 -protobuf==3.20.1; python_version >= "3.7" -pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.6" +multiprocess==0.70.14; python_version >= "3.7" +mypy-boto3-stepfunctions==1.26.21; python_version >= "3.7" +numpy==1.23.4 +packaging==23.0; python_version >= "3.7" +pandas==1.5.1; python_version >= "3.8" +pathos==0.3.0; python_version >= "3.7" +pox==0.3.2; python_version >= "3.7" +ppft==1.7.6.6; python_version >= "3.7" +protobuf3-to-dict==0.1.5; python_version >= "3.6" +protobuf==3.20.3; python_version >= "3.7" +pydantic==1.10.4; python_version >= "3.7" python-dateutil==2.8.2; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8" -pytz==2022.1; python_version >= "3.8" +pytz==2022.5; python_version >= "3.8" pyyaml==6.0; python_version >= "3.6" requests==2.28.1; python_version >= "3.7" and python_version < "4" s3transfer==0.6.0; python_version >= "3.7" -sagemaker==2.75.1 -six==1.16.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" and python_version >= "3.8" -smdebug-rulesconfig==1.0.1; python_version >= "2.7" +sagemaker==2.116.0; python_version >= "3.6" +schema==0.7.5; python_version >= "3.6" +six==1.16.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8" +smdebug-rulesconfig==1.0.1; python_version >= "3.6" stepfunctions==2.3.0 -urllib3==1.26.11; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" -zipp==3.8.1; python_version >= "3.7" +types-awscrt==0.16.10; python_version >= "3.7" and python_version < "4.0" +types-s3transfer==0.6.0.post5; python_version >= "3.7" and python_version < "4.0" +typing-extensions==4.4.0; python_full_version >= "3.7.4" and python_full_version < "4.0.0" and python_version >= "3.7" +urllib3==1.26.12; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" +zipp==3.10.0; python_version >= "3.7" diff --git a/backend/tests/functions/__init__.py b/backend/tests/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/functions/assets/__init__.py b/backend/tests/functions/assets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/functions/assets/upload_asset_workflow/__init__.py b/backend/tests/functions/assets/upload_asset_workflow/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/functions/assets/upload_asset_workflow/test_lambda_handler.py b/backend/tests/functions/assets/upload_asset_workflow/test_lambda_handler.py new file mode 100644 index 00000000..9ea8fc3c --- /dev/null +++ b/backend/tests/functions/assets/upload_asset_workflow/test_lambda_handler.py @@ -0,0 +1,79 @@ +import json +from unittest.mock import patch +from backend.functions.assets.upload_asset_workflow.request_handler import UploadAssetWorkflowRequestHandler +from backend.models.assets import ( + AssetPreviewLocationModel, UploadAssetModel, UploadAssetWorkflowRequestModel, UploadAssetWorkflowResponseModel +) +from moto import mock_stepfunctions +import pytest + + +@pytest.fixture() +def sample_request(): + event = {'body': {}} + requst = json.dumps(UploadAssetWorkflowRequestModel( + uploadAssetBody=UploadAssetModel( + databaseId='1', + assetId='test', + bucket='test_bucket', + key='test_file', + assetType='step', + description='Testing', + isDistributable=False, + specifiedPipelines=[], + Comment='Testing', + previewLocation=AssetPreviewLocationModel( + Bucket='test_bucket', + Key='test_preview_key' + ) + ) + ).dict() + ) + event['body'] = requst + return event + + +def mock_process_request(self, request): + return UploadAssetWorkflowResponseModel(message='Success') + + +def mock_process_request_returns_error(self, request): + raise Exception('StepFunction') + + +@patch.object(UploadAssetWorkflowRequestHandler, "process_request", mock_process_request) +@mock_stepfunctions +def test_request_handler_success(sample_request, monkeypatch): + monkeypatch.setenv("UPLOAD_WORKFLOW_ARN", "TestArn") + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + from backend.functions.assets.upload_asset_workflow.lambda_handler import lambda_handler + response = lambda_handler(sample_request, None) + assert response['statusCode'] == 200 + + +@patch.object(UploadAssetWorkflowRequestHandler, "process_request", mock_process_request_returns_error) +@mock_stepfunctions +def test_request_handler_500(sample_request, monkeypatch): + monkeypatch.setenv("UPLOAD_WORKFLOW_ARN", "TestArn") + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + from backend.functions.assets.upload_asset_workflow.lambda_handler import lambda_handler + response = lambda_handler(sample_request, None) + assert response['statusCode'] == 500 + + +@mock_stepfunctions +def test_request_handler_validation_error(monkeypatch): + monkeypatch.setenv("UPLOAD_WORKFLOW_ARN", "TestArn") + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + from backend.functions.assets.upload_asset_workflow.lambda_handler import lambda_handler + response = lambda_handler({'body': {}}, None) + assert response['statusCode'] == 422 + + +@mock_stepfunctions +def test_request_handler_exception(monkeypatch, sample_request): + monkeypatch.setenv("UPLOAD_WORKFLOW_ARN", "TestArn") + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + from backend.functions.assets.upload_asset_workflow.lambda_handler import lambda_handler + response = lambda_handler(sample_request, None) + assert response['statusCode'] == 500 diff --git a/backend/tests/functions/assets/upload_asset_workflow/test_request_handler.py b/backend/tests/functions/assets/upload_asset_workflow/test_request_handler.py new file mode 100644 index 00000000..6410806c --- /dev/null +++ b/backend/tests/functions/assets/upload_asset_workflow/test_request_handler.py @@ -0,0 +1,56 @@ +from backend.functions.assets.upload_asset_workflow.request_handler import UploadAssetWorkflowRequestHandler +from backend.models.assets import AssetPreviewLocationModel, UploadAssetModel, UploadAssetWorkflowRequestModel +import boto3 +from moto import mock_stepfunctions +import pytest +from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID + +simple_definition = ( + '{"Comment": "An example of the Amazon States Language using a choice state.",' + '"StartAt": "DefaultState",' + '"States": ' + '{"DefaultState": {"Type": "Fail","Error": "DefaultStateError","Cause": "No Matches!"}}}' +) + + +def _get_default_role(): + return "arn:aws:iam::" + ACCOUNT_ID + ":role/unknown_sf_role" + + +@pytest.fixture() +def sample_request(): + return UploadAssetWorkflowRequestModel( + uploadAssetBody=UploadAssetModel( + databaseId='1', + assetId='test', + bucket='test_bucket', + key='test_file', + assetType='step', + description='Testing', + specifiedPipelines=[], + isDistributable=False, + Comment='Testing', + previewLocation=AssetPreviewLocationModel( + Bucket='test_bucket', + Key='test_preview_key' + ) + ) + ) + + +@mock_stepfunctions +def test_lambda_handler_happy(sample_request): + client = boto3.client("stepfunctions", region_name='us-east-1') + sm = client.create_state_machine( + name="name", definition=str(simple_definition), roleArn=_get_default_role() + ) + request_handler = UploadAssetWorkflowRequestHandler( + sfn_client=client, + state_machine_arn=sm['stateMachineArn'] + ) + response = request_handler.process_request(sample_request) + + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + print(executions) + assert len(executions['executions']) == 1 + assert response.message == 'Success' diff --git a/backend/tests/models/__init__.py b/backend/tests/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/models/assets/__init__.py b/backend/tests/models/assets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/models/assets/test_common.py b/backend/tests/models/assets/test_common.py new file mode 100644 index 00000000..043bffbc --- /dev/null +++ b/backend/tests/models/assets/test_common.py @@ -0,0 +1,33 @@ +from backend.models.assets import ( + AssetPreviewLocationModel, + GetUploadAssetWorkflowStepFunctionInput, + UploadAssetModel, + UploadAssetWorkflowRequestModel +) +import pytest + + +@pytest.fixture() +def sample_request(): + return UploadAssetWorkflowRequestModel( + uploadAssetBody=UploadAssetModel( + databaseId='1', + assetId='test', + bucket='test_bucket', + key='test_file', + assetType='step', + description='Testing', + isDistributable=False, + specifiedPipelines=[], + Comment='Testing', + previewLocation=AssetPreviewLocationModel( + Bucket='test_bucket', + Key='test_preview_key' + ) + ) + ) + + +def test_step_function_input_from_request(sample_request): + result = GetUploadAssetWorkflowStepFunctionInput(sample_request) + assert result is not None diff --git a/backend/tests/test_sample.py b/backend/tests/test_sample.py deleted file mode 100644 index 4b9f3e3e..00000000 --- a/backend/tests/test_sample.py +++ /dev/null @@ -1,10 +0,0 @@ -from backend.functions.assets.uploadAssetWorkflow import add - - -def test_sample(): - assert 1 == 1 - - -def test_test(): - c = add(1, 2) - assert c == 3 diff --git a/infra/lib/api-builder.ts b/infra/lib/api-builder.ts index 9321c9d5..fefd486a 100644 --- a/infra/lib/api-builder.ts +++ b/infra/lib/api-builder.ts @@ -334,7 +334,7 @@ export function apiBuilder( scope, uploadAssetWorkflowStateMachine ); - attachFunctionToApi(scope, runWorkflowFunction, { + attachFunctionToApi(scope, uploadAssetWorkflowFunction, { routePath: "/assets/uploadAssetWorkflow", method: apigwv2.HttpMethod.POST, api: api.apiGatewayV2, diff --git a/infra/lib/lambdaBuilder/assetFunctions.ts b/infra/lib/lambdaBuilder/assetFunctions.ts index c404b649..c10ca57c 100644 --- a/infra/lib/lambdaBuilder/assetFunctions.ts +++ b/infra/lib/lambdaBuilder/assetFunctions.ts @@ -192,16 +192,19 @@ export function buildUploadAssetWorkflowFunction( scope: Construct, uploadAssetWorkflowStateMachine: sfn.StateMachine ): lambda.Function { - const name = "uploadAssetWorkflow" + const name = "upload_asset_workflow" //TODO: Need to send separpate PR for actual code. //TODO: Currently only passing this as part of the infra change. const uploadAssetWorkflowFunction = new lambda.DockerImageFunction(scope, name, { code: lambda.DockerImageCode.fromImageAsset(path.join(__dirname, `../../../backend/`),{ - cmd: [`backend.handlers.assets.${name}.lambda_handler`], + cmd: [`backend.functions.assets.${name}.lambda_handler.lambda_handler`], }), timeout: Duration.minutes(15), memorySize: 3008, + environment: { + UPLOAD_WORKFLOW_ARN: uploadAssetWorkflowStateMachine.stateMachineArn, + }, }) uploadAssetWorkflowStateMachine.grantStartExecution(uploadAssetWorkflowFunction) diff --git a/web/logo_dark.png b/web/logo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f728d179837b76378bfc29844ce519009c25921c GIT binary patch literal 10577 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 OjI5-RM3tCP@c#hYmP}6o literal 0 HcmV?d00001